diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2d8f714 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,880 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitreader" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd859c9d97f7c468252795b35aeccc412bdbb1e90ee6969c4fa6328272eaeff" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "clap" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "serde", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derivative", + "fnv", + "humantime", + "libc", + "log", + "log-mdc", + "once_cell", + "parking_lot", + "rand", + "serde", + "serde-value", + "serde_json", + "serde_yaml", + "thiserror", + "thread-id", + "typemap-ors", + "winapi", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "notepad_parser" +version = "0.1.0" +dependencies = [ + "byteorder", + "clap", + "csv", + "glob", + "log", + "log4rs", + "serde", + "serde_json", + "thiserror", + "winparsingtools", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "thread-id" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "typemap-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +dependencies = [ + "unsafe-any-ors", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unsafe-any-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +dependencies = [ + "destructure_traitobject", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.72", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winparsingtools" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe95c30b77a99daa589db2ecf496492a728d8ea6f773203e73fc7737217e184" +dependencies = [ + "bitreader", + "byteorder", + "chrono", + "encoding_rs", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1fb4825 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "notepad_parser" +version = "0.1.0" +edition = "2021" +description = "Notepad TabState file parser" +homepage = "https://u0041.co/posts/articals/exploring-windows-artifacts-notepad-files/" +repository = "https://github.com/AbdulRhmanAlfaifi/notepad_parser" +authors = ["AbdulRhman Alfaifi <@A__ALFAIFI>"] +license = "MIT OR Apache-2.0" +default-run = "notepad_parser" +keywords = ["DFIR", "artifacts", "forensics", "windows", "notepad", ""] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "notepad_parser" +path = "src/lib.rs" + +[[bin]] +name = "notepad_parser" +path = "src/bin/notepad_parser.rs" + +[dependencies] +winparsingtools = "^2.1.0" +byteorder = "^1.3" +serde_json = "^1.0" +serde = { version = "^1.0", features = ["derive"] } +thiserror = "^1.0.63" + +# CLI deps +clap = {version = "^4.5.15"} +glob = "^0.3.1" +csv = "^1.3.0" +log4rs = "^1.3.0" +log = "^0.4.22" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4aec2f3 --- /dev/null +++ b/README.md @@ -0,0 +1,245 @@ +# Notepad TabState parser +This is a parser for Windows 11 `TabState` artifact. This project is a library and a parser. +You can read more about the artifact structure in my blog: https://u0041.co/posts/articals/exploring-windows-artifacts-notepad-files/ + +## Install +You can install the parsers in three ways: +### Cargo +Run the following command: +```bash +cargo install notepad_parser +``` + +### Release +Download the latest precomiled version from release + +### From source +Clone and build from source by executing the following commands: +```bash +git clone https://github.com/AbdulRhmanAlfaifi/notepad_parser +cd notepad_parser +cargo build --release +target\release\notepad_parser.exe +``` + +## Usage +The following is the help message for the parser: +```bash +Created By: AbdulRhman Alfaifi +Version: v0.1.0 +Reference: https://u0041.co/posts/articals/exploring-windows-artifacts-notepad-files/ + +Notepad TabState file parser + +Usage: notepad_parser.exe [OPTIONS] [FILE] + +Arguments: + [FILE] Path the files to parse. Accepts glob. [default: C:\Users\*\AppData\Local\Packages\Microsoft.WindowsNotepad_8wekyb3d8bbwe\LocalState\TabState\????????-????-????-????-????????????.bin] + +Options: + -f, --output-format Specifiy the output format [default: jsonl] [possible values: jsonl, csv] + -o, --output-path Specifiy the output file [default: stdout] + -l, --log-level Level for logs [default: quiet] [possible values: trace, debug, info, error, quiet] + -h, --help Print help + -V, --version Print version +``` + +## Example output +### Doesn't Contains Unsaved Chunks +```json +{ + "tabstate_path": "C:\\Users\\u0041\\AppData\\Local\\Packages\\Microsoft.WindowsNotepad_8wekyb3d8bbwe\\LocalState\\TabState\\79f851b1-e2d3-45ad-82d4-b69c87c40eeb.bin", + "seq_number": 0, + "is_saved_file": true, + "path_size": 25, + "path": "C:\\Windows\\Temp\\u0041.txt", + "file_size": 25, + "encoding": "UTF8", + "cr_type": "CRLF", + "last_write_time": "2024-08-16T20:49:42Z", + "file_hash": "0039C19E2071A4BD7D355CE381B218966A12016EA11FCACB34C3A3F0A6E5D385", + "cursor_start": 25, + "cursor_end": 25, + "config_block": { + "word_wrap": true, + "rtl": false, + "show_unicode": false, + "version": 2, + "unknown0": 1, + "unknown1": 1 + }, + "file_content_size": 25, + "file_content": "This is a test file saved", + "contain_unsaved_data": false, + "checksum": "A49DA5D2" +} +``` +### Contain Unsaved Chunks +```json +{ + "seq_number": 0, + "is_saved_file": true, + "path_size": 24, + "path": "C:\\Windows\\Temp\\test.txt", + "file_size": 32, + "encoding": "UTF8", + "cr_type": "CRLF", + "last_write_time": "2024-08-08T22:18:57Z", + "file_hash": "C60D8FFBD2FF969A36BFFCA31F609E801E8E0B8DE41568E948DBEBAC1BD9B2E4", + "cursor_start": 31, + "cursor_end": 31, + "config_block": { + "word_wrap": true, + "rtl": false, + "show_unicode": false, + "version": 2, + "unknown0": 1, + "unknown1": 1 + }, + "file_content_size": 31, + "file_content": "File saved test\rFile saved test", + "contain_unsaved_data": false, + "checksum": "F44C93E7", + "unsaved_chunks": [ + { + "position": 31, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "\r", + "checksum": "90FEE334" + }, + { + "position": 32, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "t", + "checksum": "4D720EDC" + }, + { + "position": 33, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "h", + "checksum": "96657A31" + }, + { + "position": 34, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "i", + "checksum": "C8DE31A0" + }, + { + "position": 35, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "s", + "checksum": "4593E2CB" + }, + { + "position": 36, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": " ", + "checksum": "6625304C" + }, + { + "position": 37, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "a", + "checksum": "B22767B8" + }, + { + "position": 38, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": " ", + "checksum": "1CE5632C" + }, + { + "position": 38, + "num_of_deletion": 1, + "num_of_addition": 0, + "checksum": "DA9AD201" + }, + { + "position": 37, + "num_of_deletion": 1, + "num_of_addition": 0, + "checksum": "D8DC6C58" + }, + { + "position": 37, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "i", + "checksum": "7AFEEDB0" + }, + { + "position": 38, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "s", + "checksum": "8D736DBB" + }, + { + "position": 39, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": " ", + "checksum": "21854A9C" + }, + { + "position": 40, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "u", + "checksum": "6419745C" + }, + { + "position": 41, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "n", + "checksum": "F04F9676" + }, + { + "position": 42, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "s", + "checksum": "488380BA" + }, + { + "position": 43, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "a", + "checksum": "0D17D9D9" + }, + { + "position": 44, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "v", + "checksum": "BAB4815F" + }, + { + "position": 45, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "e", + "checksum": "E63BE97D" + }, + { + "position": 46, + "num_of_deletion": 0, + "num_of_addition": 1, + "data": "d", + "checksum": "B880A2EC" + } + ], + "unsaved_chunks_str": "[31]:\rthis a is unsaved" +} +``` \ No newline at end of file diff --git a/samples/not_saved/arabic/rtl_set/bc0695dd-4239-47fd-9ca1-5b5d5087575a.bin b/samples/not_saved/arabic/rtl_set/bc0695dd-4239-47fd-9ca1-5b5d5087575a.bin new file mode 100644 index 0000000..7a56f25 Binary files /dev/null and b/samples/not_saved/arabic/rtl_set/bc0695dd-4239-47fd-9ca1-5b5d5087575a.bin differ diff --git a/samples/not_saved/english/rtl_unset/9f7c2ef9-5635-4629-a936-d388ca307aac.bin b/samples/not_saved/english/rtl_unset/9f7c2ef9-5635-4629-a936-d388ca307aac.bin new file mode 100644 index 0000000..772591f Binary files /dev/null and b/samples/not_saved/english/rtl_unset/9f7c2ef9-5635-4629-a936-d388ca307aac.bin differ diff --git a/samples/saved/arabic/rtl_set/big_file/9cb51381-03ff-4d89-9cb7-08ff36f69ea8.bin b/samples/saved/arabic/rtl_set/big_file/9cb51381-03ff-4d89-9cb7-08ff36f69ea8.bin new file mode 100644 index 0000000..057813b Binary files /dev/null and b/samples/saved/arabic/rtl_set/big_file/9cb51381-03ff-4d89-9cb7-08ff36f69ea8.bin differ diff --git a/samples/saved/arabic/rtl_set/c5bf63ad-799d-4d39-a41a-fcc280410497.bin b/samples/saved/arabic/rtl_set/c5bf63ad-799d-4d39-a41a-fcc280410497.bin new file mode 100644 index 0000000..23abb88 Binary files /dev/null and b/samples/saved/arabic/rtl_set/c5bf63ad-799d-4d39-a41a-fcc280410497.bin differ diff --git a/samples/saved/arabic/unsaved_mod/c5bf63ad-799d-4d39-a41a-fcc280410497.bin b/samples/saved/arabic/unsaved_mod/c5bf63ad-799d-4d39-a41a-fcc280410497.bin new file mode 100644 index 0000000..0ffb5b2 Binary files /dev/null and b/samples/saved/arabic/unsaved_mod/c5bf63ad-799d-4d39-a41a-fcc280410497.bin differ diff --git a/samples/saved/english/rtl_unset/22bb38a5-0bf4-4ff5-8aec-b821a534b3e0.bin b/samples/saved/english/rtl_unset/22bb38a5-0bf4-4ff5-8aec-b821a534b3e0.bin new file mode 100644 index 0000000..d4a61c1 Binary files /dev/null and b/samples/saved/english/rtl_unset/22bb38a5-0bf4-4ff5-8aec-b821a534b3e0.bin differ diff --git a/samples/saved/english/rtl_unset/big_file/370b5af2-8e7c-4e6b-998b-4aaa5d21931c.bin b/samples/saved/english/rtl_unset/big_file/370b5af2-8e7c-4e6b-998b-4aaa5d21931c.bin new file mode 100644 index 0000000..930b888 Binary files /dev/null and b/samples/saved/english/rtl_unset/big_file/370b5af2-8e7c-4e6b-998b-4aaa5d21931c.bin differ diff --git a/samples/saved/english/unsaved_mod/22bb38a5-0bf4-4ff5-8aec-b821a534b3e0.bin b/samples/saved/english/unsaved_mod/22bb38a5-0bf4-4ff5-8aec-b821a534b3e0.bin new file mode 100644 index 0000000..9a3d62f Binary files /dev/null and b/samples/saved/english/unsaved_mod/22bb38a5-0bf4-4ff5-8aec-b821a534b3e0.bin differ diff --git a/src/bin/notepad_parser.rs b/src/bin/notepad_parser.rs new file mode 100644 index 0000000..4430e43 --- /dev/null +++ b/src/bin/notepad_parser.rs @@ -0,0 +1,336 @@ +use clap::{value_parser, Arg, Command}; +use csv::WriterBuilder; +use glob::glob; +use notepad_parser::{ + enums::{CRType, Encoding}, + errors::NotepadErrors, + NotepadTabStat, +}; +use serde::Serialize; +use serde_json; +use std::{ + convert::From, + fs::File, + io::{self, Write}, + process::exit, +}; + +use log::*; +use log4rs::{ + append::console::{ConsoleAppender, Target}, + config::{Appender, Root}, + encode::pattern::PatternEncoder, + Config, +}; + +use winparsingtools::date_time::FileTime; + +enum OutputFormat { + JSONL, + CSV, +} + +impl From<&str> for OutputFormat { + fn from(value: &str) -> Self { + match value { + "jsonl" => OutputFormat::JSONL, + "csv" => OutputFormat::CSV, + _ => OutputFormat::JSONL, + } + } +} + +#[derive(Debug, Serialize)] +struct CsvRecord { + tabstate_path: Option, + is_saved_file: bool, + path_size: u64, + path: Option, + file_size: Option, + encoding: Option, + cr_type: Option, + last_write_time: Option, + file_hash: Option, + cursor_start: Option, + cursor_end: Option, + word_wrap: bool, + rtl: bool, + show_unicode: bool, + version: u64, + file_content_size: u64, + file_content: String, + contain_unsaved_data: bool, + checksum: String, + unsaved_chunks_str: Option, + raw: String, +} + +impl From for CsvRecord { + fn from(value: NotepadTabStat) -> Self { + let json_data = match serde_json::to_string(&value) { + Ok(data) => data, + Err(e) => e.to_string(), + }; + Self { + tabstate_path: value.tabstate_path, + is_saved_file: value.is_saved_file, + path_size: value.path_size, + path: value.path, + file_size: value.file_size, + encoding: value.encoding, + cr_type: value.cr_type, + last_write_time: value.last_write_time, + file_hash: value.file_hash, + cursor_start: value.cursor_start, + cursor_end: value.cursor_end, + word_wrap: value.config_block.word_wrap, + rtl: value.config_block.rtl, + show_unicode: value.config_block.show_unicode, + version: value.config_block.version, + file_content_size: value.file_content_size, + file_content: value.file_content, + contain_unsaved_data: value.contain_unsaved_data, + checksum: value.checksum, + unsaved_chunks_str: value.unsaved_chunks_str, + raw: json_data, + } + } +} + +fn init_logger(level: log::LevelFilter) -> log4rs::Handle { + let log_format = "{d(%Y-%m-%d %H:%M:%S)(utc)} [{t}:{L:<3}] {h({l:<5})} {m}\n"; + + let stderr = ConsoleAppender::builder() + .target(Target::Stderr) + .encoder(Box::new(PatternEncoder::new(log_format))) + .build(); + + // Log Trace level output to file where trace is the default level + // and the programmatically specified level to stderr + + let config_builder = + Config::builder().appender(Appender::builder().build("stderr", Box::new(stderr))); + + let root_builder = Root::builder().appender("stderr"); + + let config = config_builder.build(root_builder.build(level)).unwrap(); + + log4rs::init_config(config).unwrap() +} + +fn main() { + let cli = Command::new(env!("CARGO_PKG_NAME")) + .version(env!("CARGO_PKG_VERSION")) + .author("AbdulRhman Alfaifi ") + .about(env!("CARGO_PKG_DESCRIPTION")) + .help_template("\ +{before-help} + +Created By: {author} +Version: v{version} +Reference: https://u0041.co/posts/articals/exploring-windows-artifacts-notepad-files/ + +{about} + +{usage-heading} {usage} + +{all-args}{after-help} +") + .arg( + Arg::new("input-file") + .value_name("FILE") + .help("Path the files to parse. Accepts glob.") + .default_value("C:\\Users\\*\\AppData\\Local\\Packages\\Microsoft.WindowsNotepad_8wekyb3d8bbwe\\LocalState\\TabState\\????????-????-????-????-????????????.bin") + .value_parser(value_parser!(String)), + ) + .arg( + Arg::new("output-format") + .short('f') + .long("output-format") + .value_name("FORMAT") + .help("Specifiy the output format") + .value_parser(["jsonl", "csv"]) + .default_value("jsonl"), + ) + .arg( + Arg::new("output-path") + .short('o') + .long("output-path") + .value_name("FILE") + .help("Specifiy the output file") + .value_parser(value_parser!(String)) + .default_value("stdout"), + ) + .arg( + Arg::new("log-level") + .short('l') + .long("log-level") + .value_name("LEVEL") + .help("Level for logs") + .value_parser(["trace", "debug", "info", "error", "quiet"]) + .default_value("quiet"), + ) + .get_matches(); + + let path = match cli.get_one::("input-file") { + Some(path) => path, + None => "C:\\Users\\*\\AppData\\Local\\Packages\\Microsoft.WindowsNotepad_8wekyb3d8bbwe\\LocalState\\TabState\\*.bin" + }; + let output_format = match cli.get_one::("output-format") { + Some(format) => OutputFormat::from(format.to_owned().as_str()), + None => OutputFormat::from("jsonl"), + }; + + let mut output_path = "stdout".to_string(); + let mut output: Box = match cli.get_one::("output-path") { + Some(path) => { + output_path = path.to_owned(); + match output_path.as_str() { + "stdout" => Box::new(io::stdout()), + path => match File::create(path) { + Ok(f) => Box::new(f), + Err(e) => { + error!( + "Unable create the output file '{}', ERROR: {}. Exiting...", + path, e + ); + exit(1); + } + }, + } + } + None => Box::new(io::stdout()), + }; + + let log_level = match cli + .get_one::("log-level") + .unwrap() + .to_owned() + .as_str() + { + "trace" => log::LevelFilter::Trace, + "debug" => log::LevelFilter::Debug, + "info" => log::LevelFilter::Info, + "error" => log::LevelFilter::Error, + _ => log::LevelFilter::Off, + }; + + init_logger(log_level); + + let mut csv_headers_printed = false; + // if let OutputFormat::CSV = output_format {} + + for entry in glob(path).expect("Failed to read glob pattern") { + match entry { + Ok(path_match) => { + let path_str = match path_match.to_str() { + Some(p) => p, + None => { + error!( + "Unable to convert from String to &str for '{}'", + path_match.to_string_lossy() + ); + continue; + } + }; + match NotepadTabStat::from_path(path_str) { + Ok(data) => match output_format { + OutputFormat::JSONL => match serde_json::to_string(&data) { + Ok(json) => match write!(output, "{}\n", json) { + Ok(_) => debug!( + "Successfully writen JSON data for the file '{}'", + path_str + ), + Err(e) => error!( + "Error while writing the JSON data for the file '{}', ERROR: {}", + path_str, + e + ), + }, + Err(e) => { + error!( + "{}", + NotepadErrors::CLIError( + e.to_string(), + format!( + "Unable to convert results to JSON for the file '{}'", + path_str + ) + ) + ); + } + }, + OutputFormat::CSV => { + let mut csv_writer = WriterBuilder::new(); + let mut csv_writer_builder; + if csv_headers_printed { + csv_writer_builder = + csv_writer.has_headers(false).from_writer(vec![]); + } else { + csv_writer_builder = + csv_writer.has_headers(true).from_writer(vec![]); + csv_headers_printed = true; + } + + let csv_record = CsvRecord::from(data); + match csv_writer_builder.serialize(csv_record) { + Ok(_) => debug!( + "Successfuly serilized CSV row for the file '{}'", + path_str + ), + Err(e) => error!( + "Unable to write CSV row, ERROR: {}, PATH: '{}'", + e, path_str + ), + } + match csv_writer_builder.flush() { + Ok(_) => trace!( + "Susseccfuly flushed the CSV record for the file '{}'", + path_str + ), + Err(e) => error!( + "Unable to flush CSV record, ERROR: {}, PATH: '{}'", + e, path_str + ), + } + + let row = match csv_writer_builder.into_inner() { + Ok(bytes) => match String::from_utf8(bytes) { + Ok(r) => r, + Err(e) => { + error!("Unable to convert CSV writer buffer to String, ERROR: {}", e); + continue; + } + }, + Err(e) => { + error!("Unable to convert CSV writer to String, ERROR: {}", e); + continue; + } + }; + match write!(output, "{}", row) { + Ok(_) => debug!( + "Successfully writen the CSV row for file '{}' to '{}'", + path_str, output_path + ), + Err(e) => error!( + "Unable to write the CSV row for file '{}' to '{}', ERROR: {}", + path_str, output_path, e + ), + } + } + }, + Err(e) => { + error!( + "{}", + NotepadErrors::CLIError( + e.to_string(), + format!("Unable to parse the file '{}'", path_str) + ) + ); + } + } + } + Err(e) => eprintln!("{:?}", e), + } + } +} diff --git a/src/enums.rs b/src/enums.rs new file mode 100644 index 0000000..63ea240 --- /dev/null +++ b/src/enums.rs @@ -0,0 +1,45 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +#[repr(u8)] +pub enum Encoding { + ANSI = 0x01, + UTF16LE = 0x02, + UTF16BE = 0x03, + UTF8BOM = 0x04, + UTF8 = 0x05, + UNKNOWN(u8), +} + +impl From for Encoding { + fn from(value: u8) -> Self { + match value { + 0x01 => Encoding::ANSI, + 0x02 => Encoding::UTF16LE, + 0x03 => Encoding::UTF16BE, + 0x04 => Encoding::UTF8BOM, + 0x05 => Encoding::UTF8, + x => Encoding::UNKNOWN(x), + } + } +} + +#[derive(Serialize, Debug)] +#[repr(u8)] +pub enum CRType { + CRLF = 0x1, + CR = 0x2, + LF = 0x3, + UNKNOWN(u8), +} + +impl From for CRType { + fn from(value: u8) -> Self { + match value { + 0x01 => CRType::CRLF, + 0x02 => CRType::CR, + 0x03 => CRType::LF, + x => CRType::UNKNOWN(x), + } + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..0318761 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NotepadErrors { + #[error("Encountered an error. Error: '{0}', Function: '{1}', Additinal: '{2}'")] + Generic(String, String, String), + #[error( + "File signature does't match the correct TabState file format. Expected 'NP', found '{0}'" + )] + Signature(String), + #[error("Unable to read data. Error: '{0}', Field: '{1}'")] + ReadError(String, String), + #[error("Unable to read data. Error: '{0}', Field: '{1}', Size: '{2}'")] + ReadErrorWithSize(String, String, String), + #[error("Unexpected value found. Expected: '{0}', Found: '{1}', Field: '{2}'")] + UnexpectedValue(String, String, String), + #[error("EoF Reached")] + EoF, + #[error("No data to parse")] + NA, + #[error("Error while opening a file. ERROR: '{0}', PATH: '{1}'")] + FileOpen(String, String), + #[error("CLI error. ERROR: '{0}', MSG: '{1}'")] + CLIError(String, String), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..048f791 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,588 @@ +/// A Library to parse Windows Notepad `TabState` artifacts +pub mod enums; +pub mod errors; +#[cfg(test)] +mod tests; +pub mod traits; +pub mod unsaved_chunks; + +use byteorder::ReadBytesExt; +use enums::{CRType, Encoding}; +use errors::NotepadErrors; +use serde::Serialize; +use std::convert::From; +use std::io::Read; +use unsaved_chunks::UnsavedChunks; +use winparsingtools::{ + date_time::FileTime, utils::bytes_to_hex, utils::read_uleb128, utils::read_utf16_string, +}; + +use std::fs::File; +use traits::ReadBool; + +#[derive(Serialize, Debug)] +pub struct ConfigBlock { + pub word_wrap: bool, + pub rtl: bool, + pub show_unicode: bool, + pub version: u64, + unknown0: u8, + unknown1: u8, +} + +impl ConfigBlock { + pub fn from_reader(reader: &mut R) -> std::result::Result { + // Read `word_wrap` feild + let word_wrap = match reader.read_bool() { + Ok(flag) => flag, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "ConfigBlock::word_wrap".to_string(), + )) + } + }; + let rtl = match reader.read_bool() { + Ok(flag) => flag, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "ConfigBlock::rtl".to_string(), + )) + } + }; + let show_unicode = match reader.read_bool() { + Ok(flag) => flag, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "ConfigBlock::show_unicode".to_string(), + )) + } + }; + + let version = match read_uleb128(reader) { + Ok(data) => data, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "ConfigBlock::version".to_string(), + )) + } + }; + + let unknown0 = match reader.read_u8() { + Ok(data) => data, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "ConfigBlock::unknown0".to_string(), + )) + } + }; + + let unknown1 = match reader.read_u8() { + Ok(data) => data, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "ConfigBlock::unknown1".to_string(), + )) + } + }; + + Ok(Self { + word_wrap, + rtl, + show_unicode, + version, + unknown0, + unknown1, + }) + } +} + +impl Default for ConfigBlock { + fn default() -> Self { + Self { + word_wrap: false, + rtl: false, + show_unicode: false, + version: 0, + unknown0: 0, + unknown1: 0, + } + } +} +/// Represents the structure for `TabState` files +#[derive(Serialize, Debug)] +#[allow(dead_code)] +pub struct NotepadTabStat { + #[serde(skip_serializing_if = "Option::is_none")] + pub tabstate_path: Option, + #[serde(skip_serializing)] + pub signature: [u8; 2], + // #[serde(skip_serializing)] + pub seq_number: u64, + pub is_saved_file: bool, + pub path_size: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub file_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub encoding: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cr_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_write_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub file_hash: Option, + #[serde(skip_serializing)] + pub unknown1: Option<[u8; 2]>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor_start: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor_end: Option, + pub config_block: ConfigBlock, + pub file_content_size: u64, + pub file_content: String, + pub contain_unsaved_data: bool, + pub checksum: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub unsaved_chunks: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub unsaved_chunks_str: Option, +} + +impl Default for NotepadTabStat { + fn default() -> Self { + Self { + tabstate_path: Option::None, + signature: [0x4E, 0x50], + seq_number: 0x00, + is_saved_file: false, + path_size: 0x01, + path: Option::None, + file_size: Option::None, + encoding: Option::None, + cr_type: Option::None, + last_write_time: Option::None, + file_hash: Option::None, + unknown1: Option::None, + cursor_start: Option::None, + cursor_end: Option::None, + config_block: ConfigBlock::default(), + file_content_size: 0, + file_content: String::from("Hello :D"), + contain_unsaved_data: false, + checksum: String::from("41414141"), + unsaved_chunks: Option::None, + unsaved_chunks_str: Option::None, + } + } +} + +impl NotepadTabStat { + /// Read the file from `path` and use `from_reader` to parse it + pub fn from_path(path: &str) -> std::result::Result { + let mut file = match File::open(path) { + Ok(file) => file, + Err(e) => return Err(NotepadErrors::FileOpen(e.to_string(), format!("{}", path))), + }; + + let mut parsed = match NotepadTabStat::from_reader(&mut file) { + Ok(data) => data, + Err(e) => { + return Err(NotepadErrors::Generic( + e.to_string(), + "NotepadTabStat::from_path".to_string(), + "Error during parsing".to_string(), + )); + } + }; + + parsed.tabstate_path = Some(String::from(path)); + + Ok(parsed) + } + + /// Parse data from reader + pub fn from_reader(reader: &mut R) -> std::result::Result { + // Read first two bytes as `signature` + let mut signature = [0u8; 2]; + if let Err(e) = reader.read_exact(&mut signature) { + return Err(NotepadErrors::ReadError( + e.to_string(), + "signature".to_string(), + )); + } + if signature != [0x4E, 0x50] { + return Err(NotepadErrors::Signature( + String::from_utf8_lossy(&signature).to_string(), + )); + } + + // Read unknown byte + let seq_number = match read_uleb128(reader) { + Ok(num) => num, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "unknown0".to_string(), + )) + } + }; + + // Read the flag `is_saved_file` + let is_saved_file = match reader.read_u8() { + Ok(flag) => match flag { + 0x0 => false, + 0x1 => true, + x => { + return Err(NotepadErrors::UnexpectedValue( + "bool <0x1|0x0>".to_string(), + format!("{}", x), + "is_saved_file".to_string(), + )) + } + }, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "is_saved_file".to_string(), + )) + } + }; + + // Read `path_size` + let path_size = match read_uleb128(reader) { + Ok(size) => size, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "path_size".to_string(), + )) + } + }; + + // If the TabState file is for a saved file, extract the additinal data + if is_saved_file { + // Read the `path` + let path = match read_utf16_string(reader, Option::Some(path_size as usize)) { + Ok(path) => path, + Err(e) => { + return Err(NotepadErrors::ReadErrorWithSize( + e.to_string(), + "path".to_string(), + path_size.to_string(), + )) + } + }; + + // Read `file_size`. File size on the disk + let file_size = match read_uleb128(reader) { + Ok(size) => size, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "file_size".to_string(), + )) + } + }; + + // Read `encoding`. The encoding used to be used by notepad to view the file + let encoding = match reader.read_u8() { + Ok(encoding) => Encoding::from(encoding), + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "encoding".to_string(), + )) + } + }; + + // Read `cr_type` field. + let cr_type = match reader.read_u8() { + Ok(cr_type) => CRType::from(cr_type), + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "cr_type".to_string(), + )) + } + }; + + // Read `last_write_time`. This is the last write timestamp for the file + let last_write_time = match read_uleb128(reader) { + Ok(timestamp) => FileTime::new(timestamp), + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "last_write_time".to_string(), + )); + } + }; + + // Read `file_hash`. This is the SHA256 hash of the file content on disk + let mut file_hash = [0u8; 32]; + if let Err(e) = reader.read_exact(&mut file_hash) { + return Err(NotepadErrors::ReadError( + e.to_string(), + "file_hash".to_string(), + )); + } + + // Read `unknown1` + let mut unknown1 = [0u8; 2]; + if let Err(e) = reader.read_exact(&mut unknown1) { + return Err(NotepadErrors::ReadError( + e.to_string(), + "unknown1".to_string(), + )); + } + + // Read `cursor_start`. This is starting point of the text selection + let cursor_start = match read_uleb128(reader) { + Ok(cs) => cs, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "cursor_start".to_string(), + )); + } + }; + + // Read `cursor_end` + let cursor_end = match read_uleb128(reader) { + Ok(ce) => ce, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "cursor_end".to_string(), + )); + } + }; + + // Read unknown2 + //TODO: Change to config block + let config_block = ConfigBlock::from_reader(reader)?; + // let mut unknown2 = [0u8; 6]; + // if let Err(e) = reader.read_exact(&mut unknown2) { + // return Err(NotepadErrors::ReadError( + // e.to_string(), + // "unknown2".to_string(), + // )); + // } + + // Read `file_content_size`. This is the size of the content in the TabState in chars not bytes + let file_content_size = match read_uleb128(reader) { + Ok(size) => size, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "file_content_size".to_string(), + )); + } + }; + + // Read `file_content`. This is the file contant inside the TabState file + let file_content = + match read_utf16_string(reader, Option::Some(file_content_size as usize)) { + Ok(data) => data, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "file_content".to_string(), + )); + } + }; + + // Read `contain_unsaved_data` + let contain_unsaved_data = match reader.read_u8() { + Ok(flag) => match flag { + 0x0 => false, + 0x1 => true, + x => { + return Err(NotepadErrors::UnexpectedValue( + "bool <0x0|0x1>".to_string(), + x.to_string(), + "contain_unsaved_data".to_string(), + )); + } + }, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "contain_unsaved_data".to_string(), + )); + } + }; + + // Read `checksum`. CRC32 checksum for the previous data starting from offset 0x3 + let mut checksum = [0u8; 4]; + if let Err(e) = reader.read_exact(&mut checksum) { + return Err(NotepadErrors::ReadError( + e.to_string(), + "checksum".to_string(), + )); + } + + let unsaved_chunks = match UnsavedChunks::from_reader(reader) { + Ok(data) => Option::Some(data), + Err(e) => match e { + NotepadErrors::NA => Option::None, + _ => { + return Err(e); + } + }, + }; + + let unsaved_chunks_str = match &unsaved_chunks { + Some(data) => Option::Some(data.to_string()), + None => Option::None, + }; + + Ok(Self { + tabstate_path: Option::None, + signature, + seq_number, + is_saved_file, + path_size, + path: Option::Some(path), + file_size: Option::Some(file_size), + encoding: Option::Some(encoding), + cr_type: Option::Some(cr_type), + last_write_time: Option::Some(last_write_time), + file_hash: Option::Some(bytes_to_hex(&file_hash.to_vec())), + unknown1: Option::Some(unknown1), + cursor_start: Option::Some(cursor_start), + cursor_end: Option::Some(cursor_end), + config_block, + file_content_size, + file_content, + contain_unsaved_data, + checksum: bytes_to_hex(&checksum.to_vec()), + unsaved_chunks, + unsaved_chunks_str, + }) + } + // File isn't saved to file + else { + // Read `cursor_start`. This is starting point of the text selection + let cursor_start = match read_uleb128(reader) { + Ok(cs) => cs, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "cursor_start".to_string(), + )); + } + }; + + // Read `cursor_end` + let cursor_end = match read_uleb128(reader) { + Ok(ce) => ce, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "cursor_end".to_string(), + )); + } + }; + // Read `unknown3` + let config_block = ConfigBlock::from_reader(reader)?; + + // Read `file_content_size`. This is the size of the content in the TabState in chars not bytes + let file_content_size = match read_uleb128(reader) { + Ok(size) => size, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "file_content_size".to_string(), + )); + } + }; + + let file_content = + match read_utf16_string(reader, Option::Some(file_content_size as usize)) { + Ok(data) => data, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "file_content".to_string(), + )); + } + }; + + // Read `contain_unsaved_data` + let contain_unsaved_data = match reader.read_u8() { + Ok(flag) => match flag { + 0x0 => false, + 0x1 => true, + x => { + return Err(NotepadErrors::UnexpectedValue( + "bool <0x0|0x1>".to_string(), + x.to_string(), + "contain_unsaved_data".to_string(), + )); + } + }, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "contain_unsaved_data".to_string(), + )); + } + }; + + // Read `checksum`. CRC32 checksum for the previous data starting from offset 0x3 + let mut checksum = [0u8; 4]; + if let Err(e) = reader.read_exact(&mut checksum) { + return Err(NotepadErrors::ReadError( + e.to_string(), + "checksum".to_string(), + )); + } + + let unsaved_chunks = match UnsavedChunks::from_reader(reader) { + Ok(data) => Option::Some(data), + Err(e) => match e { + NotepadErrors::NA => Option::None, + _ => { + return Err(e); + } + }, + }; + + let unsaved_chunks_str = match &unsaved_chunks { + Some(data) => Option::Some(data.to_string()), + None => Option::None, + }; + + Ok(Self { + tabstate_path: Option::None, + signature, + seq_number, + is_saved_file, + path_size, + path: Option::None, + file_size: Option::None, + encoding: Option::None, + cr_type: Option::None, + last_write_time: Option::None, + file_hash: Option::None, + unknown1: Option::None, + cursor_start: Some(cursor_start), + cursor_end: Some(cursor_end), + file_content_size, + config_block, + file_content, + contain_unsaved_data, + checksum: bytes_to_hex(&checksum.to_vec()), + unsaved_chunks, + unsaved_chunks_str, + }) + } + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..47dfbfb --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,392 @@ +use crate::unsaved_chunks::UnsavedChunks; +use crate::NotepadTabStat; +use glob::glob; +use serde_json; + +const SAMPLES_DIR_NAME: &str = "samples"; + +//Start: Utils +fn get_paths_from_glob(glob_path: &str) -> Vec { + let res = glob(glob_path) + .unwrap() + .into_iter() + .map(|x| x.unwrap().to_string_lossy().to_string()) + .collect::>(); + + if res.len() == 0 { + panic!("Glob list is empty!"); + } + + res +} + +fn check_rtl(data: &NotepadTabStat) -> bool { + data.config_block.rtl +} + +fn check_word_wrap(data: &NotepadTabStat) -> bool { + data.config_block.word_wrap +} + +fn check_unsaved_chunks(data: &NotepadTabStat) -> bool { + match data.unsaved_chunks { + Some(_) => true, + None => false, + } +} + +fn check_is_saved(data: &NotepadTabStat) -> bool { + data.is_saved_file +} + +#[allow(dead_code)] +fn check_contain_unsaved_data(data: &NotepadTabStat) -> bool { + data.contain_unsaved_data +} + +// End: Utils + +#[cfg(test)] +#[test] +fn tabstate_no_path() { + let data: [u8; 0x3D] = [ + 0x4E, 0x50, 0x00, 0x00, 0x01, 0x15, 0x15, 0x01, 0x00, 0x00, 0x02, 0x01, 0x01, 0x15, 0x50, + 0x00, 0x61, 0x00, 0x73, 0x00, 0x73, 0x00, 0x77, 0x00, 0x6F, 0x00, 0x72, 0x00, 0x64, 0x00, + 0x20, 0x00, 0x69, 0x00, 0x73, 0x00, 0x20, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, 0x64, + 0x00, 0x20, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x01, 0xDD, 0xBD, 0x91, + 0xE1, + ]; + let mut reader = &data[..]; + let res = NotepadTabStat::from_reader(&mut reader).unwrap(); + let json = serde_json::to_string_pretty(&res).unwrap(); + println!("{}", json); +} + +#[cfg(test)] +#[test] +fn tabstate_has_path_arabic_test() { + let data: [u8; 0xB6] = [ + 0x4E, 0x50, 0x00, 0x01, 0x19, 0x43, 0x00, 0x3A, 0x00, 0x5C, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x5C, 0x00, 0x54, 0x00, 0x65, + 0x00, 0x6D, 0x00, 0x70, 0x00, 0x5C, 0x00, 0x2A, 0x06, 0x2C, 0x06, 0x31, 0x06, 0x28, 0x06, + 0x29, 0x06, 0x2E, 0x00, 0x74, 0x00, 0x78, 0x00, 0x74, 0x00, 0x2C, 0x02, 0x01, 0x97, 0x83, + 0x84, 0x89, 0xDE, 0xB8, 0xB9, 0xED, 0x01, 0xA0, 0x41, 0x6E, 0xAD, 0x5D, 0xC8, 0x6E, 0xDD, + 0xFD, 0x52, 0x8D, 0x13, 0x72, 0x36, 0x1A, 0x8D, 0xEA, 0xC6, 0x5D, 0x32, 0x92, 0x83, 0x6B, + 0x0E, 0x51, 0x5D, 0x1D, 0x31, 0x1C, 0x0F, 0xCA, 0x8A, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x02, 0x01, 0x01, 0x15, 0x47, 0x06, 0x30, 0x06, 0x4A, 0x06, 0x20, 0x00, 0x2A, 0x06, + 0x2C, 0x06, 0x31, 0x06, 0x28, 0x06, 0x29, 0x06, 0x2C, 0x00, 0x20, 0x00, 0x27, 0x06, 0x44, + 0x06, 0x45, 0x06, 0x44, 0x06, 0x41, 0x06, 0x20, 0x00, 0x45, 0x06, 0x2D, 0x06, 0x41, 0x06, + 0x48, 0x06, 0x00, 0x22, 0x1F, 0x14, 0x5E, 0x00, 0x00, 0x01, 0x37, 0x06, 0xBE, 0x84, 0x98, + 0x2B, 0x00, 0x01, 0x00, 0xE6, 0x5A, 0xE8, 0x53, 0x15, 0x00, 0x01, 0x38, 0x06, 0x91, 0x1C, + 0x9C, 0x16, + ]; + let mut reader = &data[..]; + let res = NotepadTabStat::from_reader(&mut reader).unwrap(); + let json = serde_json::to_string_pretty(&res).unwrap(); + println!("{}", json); +} + +#[cfg(test)] +#[test] +fn tabstate_has_path_english_contain_unsaved_chunks_test() { + let data: [u8; 0x15F] = [ + 0x4E, 0x50, 0x00, 0x01, 0x18, 0x43, 0x00, 0x3A, 0x00, 0x5C, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x5C, 0x00, 0x54, 0x00, 0x65, + 0x00, 0x6D, 0x00, 0x70, 0x00, 0x5C, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x2E, 0x00, 0x74, 0x00, 0x78, 0x00, 0x74, 0x00, 0x20, 0x05, 0x01, 0xE1, 0x8F, 0xA1, 0xB4, + 0x8F, 0xBC, 0xBA, 0xED, 0x01, 0xC6, 0x0D, 0x8F, 0xFB, 0xD2, 0xFF, 0x96, 0x9A, 0x36, 0xBF, + 0xFC, 0xA3, 0x1F, 0x60, 0x9E, 0x80, 0x1E, 0x8E, 0x0B, 0x8D, 0xE4, 0x15, 0x68, 0xE9, 0x48, + 0xDB, 0xEB, 0xAC, 0x1B, 0xD9, 0xB2, 0xE4, 0x00, 0x01, 0x1F, 0x1F, 0x01, 0x00, 0x00, 0x02, + 0x01, 0x01, 0x1F, 0x46, 0x00, 0x69, 0x00, 0x6C, 0x00, 0x65, 0x00, 0x20, 0x00, 0x73, 0x00, + 0x61, 0x00, 0x76, 0x00, 0x65, 0x00, 0x64, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, + 0x00, 0x74, 0x00, 0x0D, 0x00, 0x46, 0x00, 0x69, 0x00, 0x6C, 0x00, 0x65, 0x00, 0x20, 0x00, + 0x73, 0x00, 0x61, 0x00, 0x76, 0x00, 0x65, 0x00, 0x64, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, + 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0xF4, 0x4C, 0x93, 0xE7, 0x1F, 0x00, 0x01, 0x0D, 0x00, + 0x90, 0xFE, 0xE3, 0x34, 0x20, 0x00, 0x01, 0x74, 0x00, 0x4D, 0x72, 0x0E, 0xDC, 0x21, 0x00, + 0x01, 0x68, 0x00, 0x96, 0x65, 0x7A, 0x31, 0x22, 0x00, 0x01, 0x69, 0x00, 0xC8, 0xDE, 0x31, + 0xA0, 0x23, 0x00, 0x01, 0x73, 0x00, 0x45, 0x93, 0xE2, 0xCB, 0x24, 0x00, 0x01, 0x20, 0x00, + 0x66, 0x25, 0x30, 0x4C, 0x25, 0x00, 0x01, 0x61, 0x00, 0xB2, 0x27, 0x67, 0xB8, 0x26, 0x00, + 0x01, 0x20, 0x00, 0x1C, 0xE5, 0x63, 0x2C, 0x26, 0x01, 0x00, 0xDA, 0x9A, 0xD2, 0x01, 0x25, + 0x01, 0x00, 0xD8, 0xDC, 0x6C, 0x58, 0x25, 0x00, 0x01, 0x69, 0x00, 0x7A, 0xFE, 0xED, 0xB0, + 0x26, 0x00, 0x01, 0x73, 0x00, 0x8D, 0x73, 0x6D, 0xBB, 0x27, 0x00, 0x01, 0x20, 0x00, 0x21, + 0x85, 0x4A, 0x9C, 0x28, 0x00, 0x01, 0x75, 0x00, 0x64, 0x19, 0x74, 0x5C, 0x29, 0x00, 0x01, + 0x6E, 0x00, 0xF0, 0x4F, 0x96, 0x76, 0x2A, 0x00, 0x01, 0x73, 0x00, 0x48, 0x83, 0x80, 0xBA, + 0x2B, 0x00, 0x01, 0x61, 0x00, 0x0D, 0x17, 0xD9, 0xD9, 0x2C, 0x00, 0x01, 0x76, 0x00, 0xBA, + 0xB4, 0x81, 0x5F, 0x2D, 0x00, 0x01, 0x65, 0x00, 0xE6, 0x3B, 0xE9, 0x7D, 0x2E, 0x00, 0x01, + 0x64, 0x00, 0xB8, 0x80, 0xA2, 0xEC, + ]; + let mut reader = &data[..]; + let res = NotepadTabStat::from_reader(&mut reader).unwrap(); + let json = serde_json::to_string_pretty(&res).unwrap(); + println!("{}", json); +} + +#[cfg(test)] +#[test] +fn tabstate_has_path_english_test() { + let data: [u8; 0xAF] = [ + 0x4E, 0x50, 0x00, 0x01, 0x18, 0x43, 0x00, 0x3A, 0x00, 0x5C, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x5C, 0x00, 0x54, 0x00, 0x65, + 0x00, 0x6D, 0x00, 0x70, 0x00, 0x5C, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x2E, 0x00, 0x74, 0x00, 0x78, 0x00, 0x74, 0x00, 0x20, 0x05, 0x01, 0xE1, 0x8F, 0xA1, 0xB4, + 0x8F, 0xBC, 0xBA, 0xED, 0x01, 0xC6, 0x0D, 0x8F, 0xFB, 0xD2, 0xFF, 0x96, 0x9A, 0x36, 0xBF, + 0xFC, 0xA3, 0x1F, 0x60, 0x9E, 0x80, 0x1E, 0x8E, 0x0B, 0x8D, 0xE4, 0x15, 0x68, 0xE9, 0x48, + 0xDB, 0xEB, 0xAC, 0x1B, 0xD9, 0xB2, 0xE4, 0x00, 0x01, 0x1F, 0x1F, 0x01, 0x00, 0x00, 0x02, + 0x01, 0x01, 0x1F, 0x46, 0x00, 0x69, 0x00, 0x6C, 0x00, 0x65, 0x00, 0x20, 0x00, 0x73, 0x00, + 0x61, 0x00, 0x76, 0x00, 0x65, 0x00, 0x64, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, + 0x00, 0x74, 0x00, 0x0D, 0x00, 0x46, 0x00, 0x69, 0x00, 0x6C, 0x00, 0x65, 0x00, 0x20, 0x00, + 0x73, 0x00, 0x61, 0x00, 0x76, 0x00, 0x65, 0x00, 0x64, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, + 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0xF4, 0x4C, 0x93, 0xE7, + ]; + let mut reader = &data[..]; + let res = NotepadTabStat::from_reader(&mut reader).unwrap(); + let json = serde_json::to_string_pretty(&res).unwrap(); + println!("{}", json); +} + +#[cfg(test)] +#[test] +fn error_test() { + let data: [u8; 0xAF] = [ + 0x41, 0x50, 0x00, 0x01, 0x18, 0x43, 0x00, 0x3A, 0x00, 0x5C, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x5C, 0x00, 0x54, 0x00, 0x65, + 0x00, 0x6D, 0x00, 0x70, 0x00, 0x5C, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x2E, 0x00, 0x74, 0x00, 0x78, 0x00, 0x74, 0x00, 0x20, 0x05, 0x01, 0xE1, 0x8F, 0xA1, 0xB4, + 0x8F, 0xBC, 0xBA, 0xED, 0x01, 0xC6, 0x0D, 0x8F, 0xFB, 0xD2, 0xFF, 0x96, 0x9A, 0x36, 0xBF, + 0xFC, 0xA3, 0x1F, 0x60, 0x9E, 0x80, 0x1E, 0x8E, 0x0B, 0x8D, 0xE4, 0x15, 0x68, 0xE9, 0x48, + 0xDB, 0xEB, 0xAC, 0x1B, 0xD9, 0xB2, 0xE4, 0x00, 0x01, 0x1F, 0x1F, 0x01, 0x00, 0x00, 0x02, + 0x01, 0x01, 0x1F, 0x46, 0x00, 0x69, 0x00, 0x6C, 0x00, 0x65, 0x00, 0x20, 0x00, 0x73, 0x00, + 0x61, 0x00, 0x76, 0x00, 0x65, 0x00, 0x64, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, + 0x00, 0x74, 0x00, 0x0D, 0x00, 0x46, 0x00, 0x69, 0x00, 0x6C, 0x00, 0x65, 0x00, 0x20, 0x00, + 0x73, 0x00, 0x61, 0x00, 0x76, 0x00, 0x65, 0x00, 0x64, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, + 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0xF4, 0x4C, 0x93, 0xE7, + ]; + let mut reader = &data[..]; + match NotepadTabStat::from_reader(&mut reader) { + Ok(_) => panic!("You shouldn't see this!"), + Err(e) => println!("{}", e), + } +} + +#[cfg(test)] +#[test] +fn tabstat_unsaved_chunks() { + // Addition at random positions + let data: [u8; 0x12A] = [ + 0x1F, 0x00, 0x01, 0x0D, 0x00, 0x90, 0xFE, 0xE3, 0x34, 0x20, 0x00, 0x01, 0x74, 0x00, 0x4D, + 0x72, 0x0E, 0xDC, 0x21, 0x00, 0x01, 0x68, 0x00, 0x96, 0x65, 0x7A, 0x31, 0x22, 0x00, 0x01, + 0x69, 0x00, 0xC8, 0xDE, 0x31, 0xA0, 0x23, 0x00, 0x01, 0x73, 0x00, 0x45, 0x93, 0xE2, 0xCB, + 0x24, 0x00, 0x01, 0x20, 0x00, 0x66, 0x25, 0x30, 0x4C, 0x25, 0x00, 0x01, 0x61, 0x00, 0xB2, + 0x27, 0x67, 0xB8, 0x26, 0x00, 0x01, 0x20, 0x00, 0x1C, 0xE5, 0x63, 0x2C, 0x26, 0x01, 0x00, + 0xDA, 0x9A, 0xD2, 0x01, 0x25, 0x01, 0x00, 0xD8, 0xDC, 0x6C, 0x58, 0x25, 0x00, 0x01, 0x69, + 0x00, 0x7A, 0xFE, 0xED, 0xB0, 0x26, 0x00, 0x01, 0x73, 0x00, 0x8D, 0x73, 0x6D, 0xBB, 0x27, + 0x00, 0x01, 0x20, 0x00, 0x21, 0x85, 0x4A, 0x9C, 0x28, 0x00, 0x01, 0x75, 0x00, 0x64, 0x19, + 0x74, 0x5C, 0x29, 0x00, 0x01, 0x6E, 0x00, 0xF0, 0x4F, 0x96, 0x76, 0x2A, 0x00, 0x01, 0x73, + 0x00, 0x48, 0x83, 0x80, 0xBA, 0x2B, 0x00, 0x01, 0x61, 0x00, 0x0D, 0x17, 0xD9, 0xD9, 0x2C, + 0x00, 0x01, 0x76, 0x00, 0xBA, 0xB4, 0x81, 0x5F, 0x2D, 0x00, 0x01, 0x65, 0x00, 0xE6, 0x3B, + 0xE9, 0x7D, 0x2E, 0x00, 0x01, 0x64, 0x00, 0xB8, 0x80, 0xA2, 0xEC, 0x16, 0x01, 0x00, 0xFE, + 0xF1, 0x37, 0x91, 0x01, 0x01, 0x00, 0xE7, 0x98, 0x82, 0x64, 0x1B, 0x00, 0x01, 0x61, 0x00, + 0xAC, 0x36, 0x61, 0x5F, 0x1C, 0x00, 0x01, 0x61, 0x00, 0x1E, 0x16, 0xBD, 0x4F, 0x1D, 0x00, + 0x01, 0x61, 0x00, 0x23, 0x76, 0x94, 0xFF, 0x1E, 0x00, 0x01, 0x62, 0x00, 0x4F, 0xFB, 0xBD, + 0xEC, 0x1F, 0x00, 0x01, 0x62, 0x00, 0x72, 0x9B, 0x94, 0x5C, 0x0C, 0x00, 0x01, 0x73, 0x00, + 0x06, 0x02, 0x5A, 0x1E, 0x0D, 0x00, 0x01, 0x73, 0x00, 0x3B, 0x62, 0x73, 0xAE, 0x0E, 0x00, + 0x01, 0x73, 0x00, 0x7C, 0xC2, 0x09, 0x7E, 0x14, 0x00, 0x01, 0x63, 0x00, 0x1C, 0x50, 0x94, + 0x0C, 0x15, 0x00, 0x01, 0x63, 0x00, 0x21, 0x30, 0xBD, 0xBC, 0x16, 0x00, 0x01, 0x63, 0x00, + 0x66, 0x90, 0xC7, 0x6C, 0x17, 0x00, 0x01, 0x63, 0x00, 0x5B, 0xF0, 0xEE, 0xDC, + ]; + let mut reader = &data[..]; + let res = UnsavedChunks::from_reader(&mut reader).unwrap(); + let json = serde_json::to_string_pretty(&res).unwrap(); + // println!("{}", res); + println!("{}", json); +} + +// Start: English language tests + +#[cfg(test)] +#[test] +fn tabstat_sample_saved_english_unsaved_mod() { + let path = format!("./{}/saved/english/unsaved_mod/*.bin", SAMPLES_DIR_NAME); + println!("AAAA"); + println!("{}", SAMPLES_DIR_NAME); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + + assert!( + check_unsaved_chunks(&data), + "Didn't extract unsaved data chunck. DATA: {:?}", + data + ); + assert!( + check_is_saved(&data), + "is_saved_file is reported to be unset, but it should be" + ); + } +} + +#[cfg(test)] +#[test] +fn tabstat_sample_saved_english_rtl_unset() { + let path = format!("./{}/saved/english/rtl_unset/*.bin", SAMPLES_DIR_NAME); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + assert!( + !check_rtl(&data), + "RTL is reported to be set, but it should't" + ); + assert!( + check_word_wrap(&data), + "WordWrap is reported to be unset, but it should be" + ); + assert!( + check_is_saved(&data), + "is_saved_file is reported to be unset, but it should be" + ); + } +} + +#[cfg(test)] +#[test] +fn tabstat_sample_saved_english_rtl_unset_big_file() { + let path = format!( + "./{}/saved/english/rtl_unset/big_file/*.bin", + SAMPLES_DIR_NAME + ); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + assert!( + !check_rtl(&data), + "RTL is reported to be set, but it should't" + ); + assert!( + check_word_wrap(&data), + "WordWrap is reported to be unset, but it should be" + ); + assert!( + check_is_saved(&data), + "is_saved_file is reported to be unset, but it should be" + ); + } +} + +#[cfg(test)] +#[test] +fn tabstat_sample_not_saved_english_rtl_unset() { + let path = format!("./{}/not_saved/english/rtl_unset/*.bin", SAMPLES_DIR_NAME); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + assert!( + !check_rtl(&data), + "RTL is reported to be set, but it should't" + ); + assert!( + !check_is_saved(&data), + "is_saved_file is reported to be set, but it should't" + ); + assert!( + check_word_wrap(&data), + "WordWrap is reported to be unset, but it should be" + ); + } +} + +// End: English language tests +// Start: Arabic language test + +#[cfg(test)] +#[test] +fn tabstat_sample_not_saved_arabic_rtl_set() { + let path = format!("./{}/not_saved/arabic/rtl_set/*.bin", SAMPLES_DIR_NAME); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + // RTL is ignored here, it is set to `true` in notepad. but it is not writen to the tabstate file. Writen after closing the window? + // assert!( + // check_rtl(&data), + // "RTL is reported to be set, but it should't" + // ); + assert!( + !check_is_saved(&data), + "is_saved_file is reported to be set, but it should't" + ); + assert!( + check_word_wrap(&data), + "WordWrap is reported to be unset, but it should be" + ); + } +} + +#[cfg(test)] +#[test] +fn tabstat_sample_saved_arabic_rtl_set() { + let path = format!("./{}/saved/arabic/rtl_set/*.bin", SAMPLES_DIR_NAME); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + // RTL is ignored here, it is set to `true` in notepad. but it is not writen to the tabstate file. Writen after closing the window? + // assert!( + // check_rtl(&data), + // "RTL is reported to be set, but it should't" + // ); + assert!( + check_is_saved(&data), + "is_saved_file is reported to be set, but it should't" + ); + assert!( + check_word_wrap(&data), + "WordWrap is reported to be unset, but it should be" + ); + } +} + +#[cfg(test)] +#[test] +fn tabstat_sample_saved_arabic_rtl_set_big_file() { + let path = format!("./{}/saved/arabic/rtl_set/big_file/*.bin", SAMPLES_DIR_NAME); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + // RTL is ignored here, it is set to `true` in notepad. but it is not writen to the tabstate file. Writen after closing the window? + // assert!( + // check_rtl(&data), + // "RTL is reported to be set, but it should't" + // ); + assert!( + check_is_saved(&data), + "is_saved_file is reported to be set, but it should't" + ); + assert!( + check_word_wrap(&data), + "WordWrap is reported to be unset, but it should be" + ); + } +} + +#[cfg(test)] +#[test] +fn tabstat_sample_saved_arabic_unsaved_mod() { + let path = format!("./{}/saved/arabic/unsaved_mod/*.bin", SAMPLES_DIR_NAME); + for path in get_paths_from_glob(&path) { + let data = NotepadTabStat::from_path(&path).unwrap(); + let json = serde_json::to_string_pretty(&data).unwrap(); + println!("{}", json); + // RTL is ignored here, it is set to `true` in notepad. but it is not writen to the tabstate file. Writen after closing the window? + // assert!( + // check_rtl(&data), + // "RTL is reported to be set, but it should't" + // ); + + assert!( + check_is_saved(&data), + "is_saved_file is reported to be set, but it should't" + ); + // Not updated if the window still open? + // assert!( + // check_contain_unsaved_data(&data), + // "contain_unsaved_data is reported to be unset, but it should be" + // ); + assert!( + check_word_wrap(&data), + "WordWrap is reported to be unset, but it should be" + ); + } +} + +// End: Arabic language test diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..e9d00c8 --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,27 @@ +use crate::errors::NotepadErrors; + +pub trait ReadBool: std::io::Read { + /// Read a `u8` and return `true` if it is `0x1` or `false` if it is `0x0`, otherwise return Error + fn read_bool(&mut self) -> std::result::Result; +} + +impl ReadBool for T { + fn read_bool(&mut self) -> std::result::Result { + let mut data = [0u8; 1]; + if let Err(e) = self.read_exact(&mut data) { + return Err(NotepadErrors::ReadError( + e.to_string(), + "traits::ReadBool".to_string(), + )); + } + match data[0] { + 0x0 => Ok(false), + 0x1 => Ok(true), + x => Err(NotepadErrors::UnexpectedValue( + "bool <0x0|0x1>".to_string(), + format!("{}", x), + "traits::ReadBool".to_string(), + )), + } + } +} diff --git a/src/unsaved_chunks.rs b/src/unsaved_chunks.rs new file mode 100644 index 0000000..b9b7d73 --- /dev/null +++ b/src/unsaved_chunks.rs @@ -0,0 +1,154 @@ +use crate::NotepadErrors; +use serde::Serialize; +use std::{ + fmt::Display, + io::{self, Read}, +}; +use winparsingtools::utils::{bytes_to_hex, read_uleb128, read_utf16_string}; + +#[derive(Debug, Serialize)] +pub struct UnsavedChunk { + position: u64, + num_of_deletion: u64, + num_of_addition: u64, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, + checksum: String, +} + +impl UnsavedChunk { + pub fn from_reader(reader: &mut R) -> std::result::Result { + // Read `position`. This is the cursor position where the data will be deleted from or added to + let position = match read_uleb128(reader) { + Ok(pos) => pos, + Err(e) => match e.kind() { + io::ErrorKind::UnexpectedEof => { + return Err(NotepadErrors::EoF); + } + _ => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "UnsavedChunk::position".to_string(), + )); + } + }, + }; + + // Read `num_of_deletion`. This is the number of characters to delete. + let num_of_deletion = match read_uleb128(reader) { + Ok(num_of_deletion) => num_of_deletion, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "UnsavedChunk::num_of_deletion".to_string(), + )); + } + }; + + // Read `num_of_addition`. This is the number of characters to add. + let num_of_addition = match read_uleb128(reader) { + Ok(num_of_addition) => num_of_addition, + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "UnsavedChunk::num_of_addition".to_string(), + )); + } + }; + + // Read `data` if it is an addition + let data = match num_of_addition { + 0 => Option::None, + _ => match read_utf16_string(reader, Option::Some(num_of_addition as usize)) { + Ok(data) => Option::Some(data), + Err(e) => { + return Err(NotepadErrors::ReadError( + e.to_string(), + "UnsavedChunk::data".to_string(), + )); + } + }, + }; + + let mut checksum = [0u8; 4]; + if let Err(e) = reader.read_exact(&mut checksum) { + return Err(NotepadErrors::ReadError( + e.to_string(), + "checksum".to_string(), + )); + } + + Ok(Self { + position, + num_of_deletion, + num_of_addition, + data, + checksum: bytes_to_hex(&checksum.to_vec()), + }) + } +} + +#[derive(Debug, Serialize)] +pub struct UnsavedChunks(Vec); + +impl UnsavedChunks { + pub fn from_reader(reader: &mut R) -> std::result::Result { + let mut unsaved_chunks: Vec = vec![]; + + loop { + match UnsavedChunk::from_reader(reader) { + Ok(chunk) => unsaved_chunks.push(chunk), + Err(e) => match e { + NotepadErrors::EoF => break, + e => { + return Err(NotepadErrors::Generic( + e.to_string(), + "UnsavedChunks::from_reader".to_string(), + "Error during reading list of UnsavedChunk.".to_string(), + )); + } + }, + } + } + + if unsaved_chunks.len() > 0 { + Ok(Self(unsaved_chunks)) + } else { + return Err(NotepadErrors::NA); + } + } +} + +impl Display for UnsavedChunks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut previous_addition = 0; + let data = self + .0 + .iter() + .map(|x| { + let mut chunk = String::from(""); + if x.num_of_addition > 0 { + if previous_addition == 0 { + previous_addition = x.position; + chunk.push_str(&format!("[{}]:{}", x.position, &x.data.clone().unwrap())); + } else if x.position == (previous_addition + 1) { + chunk.push_str(&x.data.clone().unwrap()); + previous_addition = x.position; + } else { + chunk.push_str(&format!(",[{}]:{}", x.position, &x.data.clone().unwrap())); + previous_addition = x.position; + } + } else { + if previous_addition > 0 { + previous_addition = previous_addition - 1; + } + chunk.push_str(&format!("", x.position)); + } + format!("{}", chunk) + }) + .collect::>() + .join(""); + + write!(f, "{}", data) + } +}