diff --git a/Cargo.lock b/Cargo.lock index fc290c4..b62a599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -35,7 +26,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.4", "once_cell", "serde", "version_check", @@ -44,13 +35,19 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -92,27 +89,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "biscuit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e28fc7c56c61743a01d0d1b73e4fed68b8a4f032ea3a2d4bb8c6520a33fc05a" +dependencies = [ + "chrono", + "data-encoding", + "num-bigint", + "num-traits", + "once_cell", + "ring", + "serde", + "serde_json", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -130,9 +128,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -145,9 +143,9 @@ dependencies = [ [[package]] name = "borrow-or-share" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "build_html" @@ -175,24 +173,24 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bzip2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" dependencies = [ "libbz2-rs-sys", ] [[package]] name = "cc" -version = "1.2.40" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -213,9 +211,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" @@ -264,9 +262,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -288,14 +286,20 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "deflate64" version = "0.1.10" @@ -304,9 +308,9 @@ checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -361,27 +365,29 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "evp" -version = "1.0.0-beta.3" +version = "1.0.0-beta.4" dependencies = [ "base64", + "biscuit", "build_html", "chrono", "getset", "html-escape", "infer", "jsonschema", - "pest", - "pest_derive", - "regex", + "markdown", + "outdir-tempdir", + "rand", "rust_xlsxwriter", "serde", "serde_json", + "serde_json_canonicalizer", "sha256", "thiserror", "tracing", + "tracing-subscriber", "uuid", - "winapi", - "zip 5.1.1", + "zip", ] [[package]] @@ -397,26 +403,26 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "a2152dbcb980c05735e2a651d96011320a949eb31a0c8b38b72645ce97dec676" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] name = "fluent-uri" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" dependencies = [ "borrow-or-share", "ref-cast", @@ -429,6 +435,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -510,14 +522,27 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -532,17 +557,16 @@ dependencies = [ "syn", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hex" @@ -570,12 +594,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -610,9 +633,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -631,9 +654,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64", "bytes", @@ -679,9 +702,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -692,9 +715,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -705,11 +728,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -720,42 +742,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -786,9 +804,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -812,17 +830,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -831,9 +838,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -851,15 +858,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -867,21 +874,21 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.33.0" +version = "0.37.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5" +checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e" dependencies = [ "ahash", - "base64", "bytecount", + "data-encoding", "email_address", "fancy-regex", "fraction", + "getrandom 0.3.4", "idna", "itoa", "num-cmp", "num-traits", - "once_cell", "percent-encoding", "referencing", "regex", @@ -889,6 +896,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "unicode-general-category", "uuid-simd", ] @@ -906,24 +914,15 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -936,9 +935,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lzma-rust2" @@ -950,6 +949,24 @@ dependencies = [ "sha2", ] +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.6" @@ -968,13 +985,22 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] @@ -1062,21 +1088,21 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "outdir-tempdir" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f326cab07f78d6e26cbae17cb1b5aa5ec3cf856bcf7e2bbd06847677907612e" +dependencies = [ + "uuid", +] + [[package]] name = "outref" version = "0.5.2" @@ -1122,49 +1148,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pest" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" -dependencies = [ - "pest", - "sha2", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1185,9 +1168,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1200,9 +1183,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" +checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" [[package]] name = "ppv-lite86" @@ -1237,18 +1220,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1285,7 +1268,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -1319,13 +1302,14 @@ dependencies = [ [[package]] name = "referencing" -version = "0.33.0" +version = "0.37.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d" +checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c" dependencies = [ "ahash", "fluent-uri", - "once_cell", + "getrandom 0.3.4", + "hashbrown", "parking_lot", "percent-encoding", "serde_json", @@ -1333,9 +1317,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1345,9 +1329,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1356,15 +1340,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -1395,20 +1379,28 @@ dependencies = [ ] [[package]] -name = "rust_xlsxwriter" -version = "0.90.2" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be778223b36bb449b2ef2df4856ced2d311680818a7310db5c5dc370170f935" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "chrono", - "zip 4.6.1", + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "rustc-demangle" -version = "0.1.26" +name = "rust_xlsxwriter" +version = "0.92.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "ca8b9faf2c68874f865272c92493e9bb811e5fdff197a56ecc4748885ec5a874" +dependencies = [ + "chrono", + "zip", +] [[package]] name = "rustversion" @@ -1422,6 +1414,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "ryu-js" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1464,6 +1462,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ + "indexmap", "itoa", "memchr", "ryu", @@ -1471,6 +1470,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_json_canonicalizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f777f77aeef456e47e75c2a4b16804b15395be5b344e2094a54965143ef1c31" +dependencies = [ + "ryu-js", + "serde", + "serde_json", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1518,6 +1528,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1544,19 +1563,19 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "subtle" @@ -1566,9 +1585,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1615,6 +1634,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.44" @@ -1636,9 +1664,9 @@ checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1646,19 +1674,16 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "pin-project-lite", - "slab", "socket2", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1678,9 +1703,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ "bitflags", "bytes", @@ -1708,9 +1733,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1719,9 +1744,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1730,11 +1755,41 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ + "log", "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1750,16 +1805,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-id" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" @@ -1775,9 +1842,9 @@ dependencies = [ [[package]] name = "utf8-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" [[package]] name = "utf8_iter" @@ -1787,14 +1854,14 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom", + "getrandom 0.3.4", "js-sys", "rand", - "serde", + "serde_core", "wasm-bindgen", ] @@ -1805,10 +1872,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" dependencies = [ "outref", - "uuid", "vsimd", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -1836,15 +1908,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1856,9 +1919,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -1867,25 +1930,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -1896,9 +1945,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1906,58 +1955,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", ] -[[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.62.2" @@ -2019,11 +2046,29 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -2032,14 +2077,31 @@ 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", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -2048,48 +2110,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2098,17 +2208,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2116,9 +2225,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -2128,18 +2237,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -2189,9 +2298,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -2200,9 +2309,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -2211,9 +2320,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -2222,23 +2331,9 @@ dependencies = [ [[package]] name = "zip" -version = "4.6.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" -dependencies = [ - "arbitrary", - "crc32fast", - "flate2", - "indexmap", - "memchr", - "zopfli", -] - -[[package]] -name = "zip" -version = "5.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" dependencies = [ "aes", "arbitrary", @@ -2247,7 +2342,7 @@ dependencies = [ "crc32fast", "deflate64", "flate2", - "getrandom", + "getrandom 0.3.4", "hmac", "indexmap", "lzma-rust2", @@ -2263,15 +2358,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "36134c44663532e6519d7a6dfdbbe06f6f8192bde8ae9ed076e9b213f0e31df7" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index ebf56f1..488c4b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "evp" description = "Library to work with evidence packages (*.evp)." -version = "1.0.0-beta.3" # 1.0.0 here means the I-D is accepted or concluded +version = "1.0.0-beta.4" # 1.0.0 here means the I-D is accepted or concluded edition = "2024" license = "GPL-3.0-or-later" authors = [ @@ -14,33 +14,31 @@ rust-version = "1.88.0" [features] default = ["exporter-html", "exporter-zip-of-files"] -exporter-excel = ["dep:rust_xlsxwriter"] -exporter-html = ["dep:build_html", "dep:html-escape"] +exporter-excel = ["dep:markdown", "dep:rust_xlsxwriter"] +exporter-html = ["dep:build_html", "dep:html-escape", "dep:markdown"] exporter-zip-of-files = [] [dependencies] base64 = "0.22.1" +biscuit = "0.7.0" build_html = { version = "2.5.0", optional = true } chrono = { version = "0.4.38", features = ["serde"] } getset = "0.1.2" html-escape = { version = "0.2.13", optional = true } infer = "0.19.0" -jsonschema = "0.33.0" -rust_xlsxwriter = { version = "0.90.0", features = ["chrono"], optional = true } +jsonschema = "0.37.4" +markdown = { version = "1.0.0", optional = true } +rust_xlsxwriter = { version = "0.92.2", features = ["chrono"], optional = true } serde = { version = "1.0.200", features = ["derive"] } serde_json = "1.0.116" +serde_json_canonicalizer = "0.3.1" sha256 = "1.5.0" thiserror = "2.0.4" tracing = "0.1.41" -uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] } -zip = "5.1.1" +uuid = { version = "1.8.0", features = ["v7", "fast-rng", "serde"] } +zip = "6.0.0" -# Remove when AngelMark deprecated -pest = "2.7.15" -# Remove when AngelMark deprecated -pest_derive = "2.7.15" -# Remove when AngelMark deprecated -regex = "1.11.1" - -[target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = ["fileapi", "winnt"] } +[dev-dependencies] +outdir-tempdir = "0.2.0" +rand = "0.9.2" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } diff --git a/schemas/manifest.2.schema.json b/schemas/manifest.2.schema.json index 539b35a..834d391 100644 --- a/schemas/manifest.2.schema.json +++ b/schemas/manifest.2.schema.json @@ -38,7 +38,7 @@ }, "required": ["title", "authors"] }, - "custom_test_case_metadata": { + "custom_metadata": { "type": "object", "description": "Custom metadata fields for test cases", "patternProperties": { @@ -91,9 +91,18 @@ "type": "string", "format": "uuid", "description": "The UUID of the test case. If present here, there MUST be an associated test case file in the `testcases` directory of the package with the name `.json`." + }, + "attestations": { + "type": "array", + "description": "An array of attestations over this test case.", + "items": { + "type": "string", + "description": "The elements within the \"attestations\" array **MUST** be JWS [RFC7515] signatures. The signature payload must be a SHA256 checksum of a copy of the test case manifest (i.e. the file \"uuid.json\"), having been processed into JSON canonical format as defined in [RFC8785].", + "pattern": "^[A-z0-9_-]+\\.[A-z0-9_-]+\\.[A-z0-9_-]+$" + } } }, - "required": ["id"] + "required": ["id", "attestations"] } } }, diff --git a/schemas/testcase.2.schema.json b/schemas/testcase.2.schema.json new file mode 100644 index 0000000..1e934a8 --- /dev/null +++ b/schemas/testcase.2.schema.json @@ -0,0 +1,72 @@ +{ + "$id": "https://evidenceangel-schemas.hpkns.uk/testcase.2.schema.json", + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "description": "A test case file `testcases/.json` as part of an evidence package.", + "properties": { + "metadata": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The title of the test case", + "minLength": 1, + "maxLength": 30 + }, + "execution_datetime": { + "type": "string", + "format": "date-time", + "description": "The date and time of the execution of this test case starting." + }, + "passed": { + "type": ["string", "null"], + "description": "The state of the test case", + "enum": [ + "pass", + "fail", + null + ] + }, + "custom": { + "type": "object", + "description": "Custom metadata values", + "patternProperties": { + ".+": { + "type": "string" + } + } + } + }, + "required": ["title", "execution_datetime"] + }, + "evidence": { + "type": "array", + "items": { + "type": "object", + "description": "A piece of evidence as part of this test case.", + "properties": { + "kind": { + "type": "string", + "description": "The Internet Media Type of the data stored.", + "pattern": "^(\\w*)\\/([\\w\\.-]*)(\\+([\\w\\.-]*))?(;((.+)=(.*);)*(.+)=(.*))?$" + }, + "value": { + "type": "string", + "description": "Either `plain:` followed by plain text, `media:` followed by a media SHA256 hash, or `base64:` followed by a base64 string of data without padding.", + "pattern": "^(plain:.*)|(media:[0-9a-f]{64})|(base64:[A-z0-9+/]*)$" + }, + "caption": { + "type": "string", + "description": "An optional caption for this piece of evidence." + }, + "original_filename": { + "type": "string", + "description": "The original filename for File evidence" + } + }, + "required": ["kind", "value"] + } + } + }, + "required": ["metadata", "evidence"] +} diff --git a/spec/build.sh b/spec/build.sh index 6399ce0..8db6c57 100755 --- a/spec/build.sh +++ b/spec/build.sh @@ -4,4 +4,4 @@ FILE="draft-hopkins-evp-spec.md" RFCXML=$(basename "$FILE" .md).xml mmark $FILE >"$RFCXML" -xml2rfc --v3 --text --html --pdf $RFCXML +xml2rfc --v3 --text --html $RFCXML diff --git a/spec/draft-hopkins-evp-spec.md b/spec/draft-hopkins-evp-spec.md index a3c1f0b..908d8d3 100644 --- a/spec/draft-hopkins-evp-spec.md +++ b/spec/draft-hopkins-evp-spec.md @@ -1,5 +1,5 @@ %%% -title = "Evidence Package Format Specification" +title = "Evidence Package Format Specification: Storing Evidence from Software Testing" abbrev = "evp-spec" ipr = "trust200902" area = "" @@ -9,11 +9,11 @@ submissionType = "independent" [seriesInfo] name = "Internet-Draft" -value = "draft-hopkins-evp-spec-04" +value = "draft-hopkins-evp-spec-08" stream = "independent" status = "informational" -date = 2025-07-27T00:00:00Z +date = 2025-12-06T00:00:00Z [[author]] initials="L." @@ -24,7 +24,7 @@ fullname="Lily Hopkins" email = "lily@hpkns.uk" [[author]] -inials="E." +initials="E." surname="Turner" fullname="Eden Turner" [author.address] @@ -34,10 +34,12 @@ fullname="Eden Turner" .# Abstract -Taking evidence is a key part of any software testing process. This -specification defines a format which collects evidence together and -stores metadata and annotations in an organised fashion from both manual -and automated testing sources. +Taking evidence is a key part of any robust software testing process. +This specification defines a format which collects evidence together +and stores metadata and annotations in an organised fashion from both +manual and automated testing sources. + +This work is not a standard and does not enjoy community consensus. {mainmatter} @@ -46,19 +48,21 @@ and automated testing sources. ## Purpose The purpose of this specification is to define a format for storage of -test evidence that: +evidence produced as the result of software testing that: * allows for basic collation of evidence; * can store any kind of file type that might be produced; * stores data compressed; * stores related evidence together, but allows for dividing up by test - case, and; + case; +* allows test evidence to be attested with a traceable list of attestors, + and; * is built upon widely available standards. The format does not attempt to: -* act as an captioned archiving solution for other purposes, even if it - may be suitable for them. +* act as an captioned archiving solution for other purposes outside of + software testing, even if it may be suitable for them. ## Intended Audience @@ -81,7 +85,7 @@ situations where writing an implementation may be desirable: ## Changes from Previous Versions -This document forms the original specification. +This document forms the original accepted specification. # Terminology @@ -93,12 +97,12 @@ when, and only when, they appear in all capitals, as shown here. # Specification -An evidence package is a structured ZIP archive [@!zip]. It **MUST** -contain the file "manifest.json", and the directories "media" and -"testcases" internally within the ZIP archive. This structure does not -need to be represented outside of the ZIP archive and as such the -internal structure does not need to be understood by an end-user of any -tool that works with evidence packages. +An evidence package is a structured ZIP archive [@!zip] using deflate +compression. It **MUST** contain the file "manifest.json", and the +directories "media" and "test_cases" internally within the ZIP archive. +This structure does not need to be represented outside of the ZIP +archive and as such the internal structure does not need to be +understood by an end-user of any tool that works with evidence packages. @@ -115,19 +119,30 @@ See (#example-archive) for an example of the file's internal structure. ## "manifest.json" File The manifest.json file defines metadata relating to the entire package -of evidence. It **MUST** be a JSON [@!RFC8259] file with the following -elements: +of evidence. It **MUST** be a UTF-8 encoded, LF line ended, JSON +[@!RFC8259] file with the following elements: | Element | Condition | Type | Section | Description | |------------|-----------|------|---|---| -| $schema | Optional | String | | The $schema element **MAY** point to a copy of the schema for the manifest. | +| $schema | Optional | String | (#manifest-schema) | The $schema element **MAY** point to a copy of the schema for the manifest. | | metadata | Mandatory | Object | (#manifest-metadata) | The metadata element stores package metadata. | -| custom_test_case_metadata | Mandatory | Object | (#manifest-custom-metadata) | Custom metadata fields for test cases in this package. | +| custom_metadata | Mandatory | Object | (#manifest-custom-metadata) | Custom metadata fields for test cases in this package. | | media | Mandatory | Array | (#manifest-media) | The media element stores a list of media files that are stored in this evidence package. | | test_cases | Mandatory | Array | (#manifest-test-cases) | The test_cases element stores a list of test cases. | See an example manifest.json file in (#example-manifest). +### "$schema" Element {#manifest-schema} + +This element **MAY** optionally be provided to point to a JSON schema +describing the structure of the file. This is typically most useful for +validation, however it **MUST** be acceptable for it to be missing, and +this specification should be seen as the primary definition of structure +over anything defined in the linked schema. + +The JSON schema provided at by this element may give details about any +additional fields used that are not defined in this specification. + ### "metadata" Element {#manifest-metadata} | Element | Condition | Type | Section | Description | @@ -142,7 +157,7 @@ See an example manifest.json file in (#example-manifest). | name | Mandatory | String | The author's name. | | email | Optional | String/Null | The author's email address, although format is not verified. | -### "custom_test_case_metadata" Element {#manifest-custom-metadata} +### "custom_metadata" Element {#manifest-custom-metadata} Elements within this object will become custom metadata properties for test cases in this package. Each object **MUST** have the following @@ -163,7 +178,7 @@ true for more than one field. The purpose of primary is not enforced as part of this specification, however it should be seen as suggesting that one custom metadata field is more useful than others, and as such may be used to influence the -information displayed to users, for example an implementor might choose +information displayed to users, for example an implementer might choose to show the primary custom metadata value for each test case alongside it. @@ -176,55 +191,132 @@ it. ### "test_cases" Array Element {#manifest-test-cases} -| Element | Condition | Type | Description | -|------------|-----------|------|---| -| id | Mandatory | String | The UUID of the test case. If present here, there **MUST** be an associated test case file in the "testcases" directory of the package with the name ".json". | +| Element | Condition | Type | Section | Description | +|------------|-----------|------|---------|---| +| id | Mandatory | String | | The UUID of the test case. If present here, there **MUST** be an associated test case file in the "test_cases" directory of the package with the name ".json". | +| attestations | Mandatory | Array of Strings | (#manifest-test-case-attestations) | An array of attestations over this test case. | + +#### "attestations" Array {#manifest-test-case-attestations} + +The elements within the "attestations" array **MUST** be JWS [@!RFC7515] +signatures. The signature payload must be a SHA256 checksum of a copy of +the test case manifest (i.e. the file "uuid.json"), having been +processed into JSON canonical format as defined in [@!RFC8785]. + +In environments where it is desirable to frequently validate +attestations, it is recommended to include the "x5c" X.509 certificate, +and a "jku" (JWT Key URL). An implementing client is **RECOMMENDED** to +display the URL in some fashion if the key passes validation as a way to +prove the signing party, however another approach may also be desirable +depending on environment. + +Implementing clients **SHOULD NOT** use symmetric key types (although it +may be acceptable for tools that are only used within a limited scope), +and APIs implementing this specification **MAY** choose to be +incompatible with symmetric types. + +As a worked example, a test case may start off like this: + +~~~json +{ + "$schema": "https://evidenceangel-schemas.hpkns.uk/testcase.2.schema.json", + "metadata": { + "title": "Test Case", + "execution_datetime": "2025-12-05T20:40:22.743821295Z", + "passed": null + }, + "evidence": [ + { + "kind": "text/plain", + "value": "plain:Hello, world!" + } + ] +} +~~~ -## "testcases" Directory +This should then be canonicalised: + +~~~json +{"$schema":"https://evidenceangel-schemas.hpkns.uk/testcase.2.schema.json","evidence":[{"kind":"text/plain","value":"plain:Hello, world!"}],"metadata":{"execution_datetime":"2025-12-05T20:40:22.743821295Z","passed":null,"title":"Test Case"}} +~~~ + +A SHA256 checksum can be generated: + +~~~text +6cd9684d866d5dacd85064f20d0b3fd423e30946c6b99d1f2defae529360becc +~~~ + +This can now be signed and the original manifest can be modified: + +~~~json +{ + "id": "7928de11-8de8-4bfe-b5b7-cbf07c7066d9", + "attestations": [ + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.YmI5YzdkMjczYWY2NzE5NWM1MWM1N2YyNzRjMDc5NTViODZiMDA3MWE0MDU3MWFjOTIwYzE2M2UzNDQxYzUwZQ.KtbRLfAh8UmSxSWYnObpydXyjGO_IPF2acU_x-eFY6dLDBD809zJm6HaTE9jjsQlnX8eGWRIOzKXTWMdgp-fXg" + ], + "some_other_value": "Added from somewhere other than this specification!" +} +~~~ + +## "test_cases" Directory The test cases directory stores the manifests for each test case within this evidence package. Each test case is stored as a JSON file, with a UUIDv4 name [@!RFC9562]. +A test case present here **MUST** have a valid entry in the manifest +"test_cases" array defined in (#manifest-test-cases). ### ".json" File | Element | Condition | Type | Section | Description | |----------|-----------|------|---|---| -| $schema | Optional | String | | The $schema element **MAY** point to a copy of the schema for the manifest. | +| $schema | Optional | String | (#test-case-schema) | The $schema element **MAY** point to a copy of the schema for the manifest. | | metadata | Mandatory | Object | (#test-case-metadata) | The metadata relating to this test case. | | evidence | Mandatory | Array | (#test-case-evidence) | The evidence within this test case. | See an example .json file in (#example-test-case). +#### "$schema" Element {#test-case-schema} + +This element **MAY** optionally be provided to point to a JSON schema +describing the structure of the file. This is typically most useful for +validation, however it **MUST** be acceptable for it to be missing, and +this specification should be seen as the primary definition of structure +over anything defined in the linked schema. + +The JSON schema provided at by this element may give details about any +additional fields used that are not defined in this specification. + #### "metadata" Element {#test-case-metadata} | Element | Condition | Type | Description | |--------------------|-----------|------|---| | title | Mandatory | String | The title of the test case. | | execution_datetime | Mandatory | String | The ISO8601 date and time of the execution of this test case starting. | -| passed | Mandatory | String | The state of the test case, if present **MUST** be either "pass", "fail", or null. If absent, it **MUST** be interpreted as null. | +| passed | Optional | Enumerated | The state of the test case, if present **MUST** be either the string "pass" or "fail", or null. If absent, it **MUST** be interpreted as null. | | custom | Mandatory | Object | Custom metadata values. | The "custom" field is used to add custom metadata that has been -specified in the package manifest's "custom_test_case_metadata" field. +specified in the package manifest's "custom_metadata" field. If a value is specified in "custom", it **MUST** be present in the package manifest, but all values in the package manifest do not need to -be present here. All values **MUST** be strings. +be present here. All values **MUST** be strings and are stored as a +simple key-value map, with the custom field ID defined in the manifest +as the key. #### "evidence" Array Element {#test-case-evidence} | Element | Condition | Type | Section | Description | |-------------------|-----------|------|---|---| -| kind | Mandatory | String | (#evidence-kind) | The type of data stored. | +| kind | Mandatory | String | (#evidence-kind) | The Internet Media Type [@!RFC2046] of data stored. | | value | Mandatory | String | (#evidence-value) | The data stored within this piece of evidence. | | caption | Optional | String/Null | | An optional caption for this piece of evidence. | -| original_filename | Optional | String/Null | | The original filename. **MAY** be provided for Image and File evidence, **MUST NOT** be provided otherwise. | +| original_filename | Optional | String/Null | | The original filename. | ##### "kind" {#evidence-kind} -The "kind" of evidence **MUST** be one of "Text", "RichText", "Image", -"Http", "File". +The "kind" of evidence **MUST** be an Internet Media Type [@!RFC2046]. For more information about each type, see (#kinds-of-evidence). @@ -249,7 +341,7 @@ manifest "media" element. In the unlikely event that there is a checksum clash, there is currently no preferred method for resolving this. The probability of such a situation is decided to be acceptably low given the expected size and -number of files stored in an evidence package, however implementors +number of files stored in an evidence package, however implementers **MAY** choose to store the clashing file as base64 data instead of as an additional media file. @@ -275,41 +367,49 @@ is no current lock over the package. Software implementing the evidence package format **MUST NOT** load files from the "media" directory into memory until it is needed for -display or for extraction. Implementors **MUST** use streams to load +display or for extraction. implementers **MUST** use streams to load media files to avoid trying to load the entire file into memory as it may not fit. # Kinds of Evidence {#kinds-of-evidence} -Evidence packages support the following kinds of evidence: +Evidence packages can support any valid Internet Media Type [@!RFC2046] +as evidence. implementers of this specification **MUST** be able to +display the following types: -| Kind | Description | -|------------|----------------------------------------------------| -| "Text" | Plain text with no formatting. | -| "RichText" | Text with very basic markdown support. | -| "Image" | An image that should be rendered where possible. | -| "Http" | An HTTP request/response pair. | -| "File" | A raw file, which may be text or binary in nature. | +| Media Type | Description | +|--------------------------|--------------------------------------------------| +| text/plain | Plain text with no formatting. | +| text/markdown | Text with markdown support. | +| text/vnd.angel.http-data | An HTTP request/response pair. | +| image/* | An image that should be rendered where possible. | -Implementors **MUST** support all of these kinds, and **MUST NOT** -introduce new kinds. +Common image formats **SHOULD** be rendered where possible, but it is +not required to support every possible type of image. -## RichText's Markdown +Markdown **SHOULD** be rendered where possible, but it may be adapted +for security reasons. If it is changed before display, a notice **MUST** +be displayed to the user disclosing that it has been adjusted for +security. For example, it is acceptable to strip raw HTML tags before +rendering. -The RichText evidence kind supports a very limited version of markdown: +Other media types **MUST** be supported insofar as being able to extract +the data from the evidence package so that they can be opened in other +software. -* Headings 1-6 -* Bold, Italic, Monospace -* Tables -* Code blocks with syntax highlighting +## HTTP Requests {#http-requests} -Implementors **MUST NOT** process any other markup. +Where text/vnd.angel.http-data is used, an HTTP request and +response **MUST** be present in plain text, and a Record Separator +character (0x1e) **MUST** be used to split the request and response +portion. In other words, the format **MUST** comply with the following +regular expression: -## HTTP Requests +~~~regex +^(?[.\r\n]*)\x1e(?[.\r\n]*)$ +~~~ -Where HTTP is used, a Record Separator character (0x1e) can be used to -split the request and response portion, for example the separator is -present at <<1>>: +For example the separator is present at <<1>>: ~~~http GET / HTTP/1.1 @@ -326,21 +426,22 @@ Connection: close Every JSON file within an evidence package **MAY** have new fields added, and as such extended behaviours **MAY** be implemented, however -implementors **MUST** be able to load an evidence package without these +implementers **MUST** be able to load an evidence package without these additional fields. -When an implementor loads a file with fields it cannot understand, it +When an implementer loads a file with fields it cannot understand, it **MUST** retain the fields on saving the file. # IANA Considerations This document acts as the specification for the media type -application/vnd.angel.evidence-package. +application/vnd.angel.evidence-package. Additionally, the media type +text/vnd.angel.http-data is defined in (#http-requests). # Security Considerations The evidence package format can store arbitrary files that may or may -not be executable. Implementors **MUST NOT** execute any file contained +not be executable. implementers **MUST NOT** execute any file contained within and **SHALL** only extract the contained files if needed. Otherwise, there are no concerns for security from the file type itself. @@ -354,7 +455,7 @@ example.evp |- manifest.json |- media | \- 203073da0b36a5921f2914e2093abcae7eb987846f405b438c25792bab1617fa - \- testcases + \- test_cases \- eabb5d31-a958-4609-ac98-83365e14d18b.json ~~~ @@ -374,7 +475,7 @@ example.evp } ] }, - "custom_test_case_metadata": { + "custom_metadata": { "example": { "name": "Example Metadata Field", "description": "A field showing that custom fields can be added", @@ -389,7 +490,10 @@ example.evp ], "test_cases": [ { - "id": "eabb5d31-a958-4609-ac98-83365e14d18b" + "id": "eabb5d31-a958-4609-ac98-83365e14d18b", + "attestations": [ + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.YmI5YzdkMjczYWY2NzE5NWM1MWM1N2YyNzRjMDc5NTViODZiMDA3MWE0MDU3MWFjOTIwYzE2M2UzNDQxYzUwZQ.KtbRLfAh8UmSxSWYnObpydXyjGO_IPF2acU_x-eFY6dLDBD809zJm6HaTE9jjsQlnX8eGWRIOzKXTWMdgp-fXg" + ] } ] } @@ -409,18 +513,24 @@ example.evp }, "evidence": [ { - "kind":"Text", + "kind":"text/plain", "value":"plain:This is some text based evidence" }, { - "kind":"Text", + "kind":"text/plain", "value":"base64:VGhpcyBpcyBzb21lIHRleHQgYmFzZWQgYmFzZTY0IGVuY29kZWQgZXZpZGVuY2U" }, { - "kind":"File", + "kind":"text/plain", "value":"media:203073da0b36a5921f2914e2093abcae7eb987846f405b438c25792bab1617fa", "caption": "An example file", "original_filename": "example.txt" + }, + { + "kind":"image/png", + "value":"media:c561967275f002e65b222b4577378f5a20a5881edd00fbe648beef6b4f4971a9", + "caption": "An example image", + "original_filename": "image.png" } ] } @@ -432,4 +542,4 @@ example.evp # JSON Schema for Test Case Manifest -<{{testcase.1.schema.json}} +<{{testcase.2.schema.json}} diff --git a/spec/manifest.2.schema.json b/spec/manifest.2.schema.json new file mode 120000 index 0000000..1237f5f --- /dev/null +++ b/spec/manifest.2.schema.json @@ -0,0 +1 @@ +../schemas/manifest.2.schema.json \ No newline at end of file diff --git a/spec/testcase.2.schema.json b/spec/testcase.2.schema.json new file mode 120000 index 0000000..353589a --- /dev/null +++ b/spec/testcase.2.schema.json @@ -0,0 +1 @@ +../schemas/testcase.2.schema.json \ No newline at end of file diff --git a/src/angelmark/angelmark.pest b/src/angelmark/angelmark.pest deleted file mode 100644 index 49c595a..0000000 --- a/src/angelmark/angelmark.pest +++ /dev/null @@ -1,100 +0,0 @@ -MarkupFile = { - SOI ~ - MarkupLine* ~ - EOI -} - -MarkupLine = _{ - Heading6 | - Heading5 | - Heading4 | - Heading3 | - Heading2 | - Heading1 | - Table | - Paragraph | - Newline -} - -Newline = { - NEWLINE+ -} - -Heading1 = { - "#" ~ " "? ~ Paragraph -} - -Heading2 = { - "##" ~ " "? ~ Paragraph -} - -Heading3 = { - "###" ~ " "? ~ Paragraph -} - -Heading4 = { - "####" ~ " "? ~ Paragraph -} - -Heading5 = { - "#####" ~ " "? ~ Paragraph -} - -Heading6 = { - "######" ~ " "? ~ Paragraph -} - -Paragraph = _{ - TextContent+ ~ Newline? -} - -TextContent = _{ - TextBold | - TextItalic | - TextMonospace | - RawText -} - -TextBold = { - "**" ~ TextContent ~ "**" -} - -TextItalic = { - ("_" ~ TextContent ~ "_") | - ("*" ~ TextContent ~ "*") -} - -TextMonospace = { - "`" ~ TextContent ~ "`" -} - -Table = { - TableRow ~ - TableAlignmentRow ~ - TableRow* -} - -TableRow = ${ - "|"? ~ TableCell ~ (!("|" ~ NEWLINE) ~ "|" ~ TableCell)+ ~ "|"? ~ NEWLINE -} - -TableAlignmentRow = ${ - "|"? ~ TableAlignmentCell ~ (!("|" ~ NEWLINE) ~ "|" ~ TableAlignmentCell)+ ~ "|"? ~ NEWLINE -} - -TableCell = ${ - TextContent* -} - -TableAlignmentCell = @{ - ":"? ~ "-"+ ~ ":"? -} - -RawText = @{ - ( - ((EscapeChar ~ (EscapeChar | "_" | "**" | "*" | "`" | "|")) | - !("*" | "_" | "`" | "|" | NEWLINE) - ) ~ ANY)+ -} - -EscapeChar = { "\\" } diff --git a/src/angelmark/error.rs b/src/angelmark/error.rs deleted file mode 100644 index f2488aa..0000000 --- a/src/angelmark/error.rs +++ /dev/null @@ -1,9 +0,0 @@ -use thiserror::Error; - -/// AngelMark errors -#[derive(Error, Debug)] -pub enum Error { - /// A parsing error - #[error("Error parsing the provided markup")] - Parsing(#[from] Box>), -} diff --git a/src/angelmark/lexer.rs b/src/angelmark/lexer.rs deleted file mode 100644 index 1af962d..0000000 --- a/src/angelmark/lexer.rs +++ /dev/null @@ -1,6 +0,0 @@ -use pest_derive::Parser; - -/// Lex AngelMark into a set of rules with this parser. -#[derive(Parser)] -#[grammar = "angelmark/angelmark.pest"] -pub struct AngelmarkParser; diff --git a/src/angelmark/line.rs b/src/angelmark/line.rs deleted file mode 100644 index 339022c..0000000 --- a/src/angelmark/line.rs +++ /dev/null @@ -1,145 +0,0 @@ -use super::{AngelmarkTable, AngelmarkText, EqIgnoringSpan, OwnedSpan}; - -/// A line of markup in AngelMark -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum AngelmarkLine { - /// A level 1 heading. - Heading1(Vec, OwnedSpan), - /// A level 2 heading. - Heading2(Vec, OwnedSpan), - /// A level 3 heading. - Heading3(Vec, OwnedSpan), - /// A level 4 heading. - Heading4(Vec, OwnedSpan), - /// A level 5 heading. - Heading5(Vec, OwnedSpan), - /// A level 6 heading. - Heading6(Vec, OwnedSpan), - /// A line of text. - TextLine(AngelmarkText, OwnedSpan), - /// A table. - Table(AngelmarkTable, OwnedSpan), - /// A line separator. - Newline(OwnedSpan), -} - -impl AngelmarkLine { - /// Get the span from this line - #[must_use] - pub fn span(&self) -> &OwnedSpan { - match self { - Self::Heading1(_, span) - | Self::Heading2(_, span) - | Self::Heading3(_, span) - | Self::Heading4(_, span) - | Self::Heading5(_, span) - | Self::Heading6(_, span) - | Self::TextLine(_, span) - | Self::Table(_, span) - | Self::Newline(span) => span, - } - } -} - -impl EqIgnoringSpan for AngelmarkLine { - /// Compare two [`AngelmarkLine`] instances, ignoring their span. - fn eq_ignoring_span(&self, other: &Self) -> bool { - match self { - Self::Heading1(inner, _) => { - if let Self::Heading1(other_inner, _) = other { - if inner.len() != other_inner.len() { - return false; - } - inner - .iter() - .zip(other_inner.iter()) - .all(|(a, b)| a.eq_ignoring_span(b)) - } else { - false - } - } - Self::Heading2(inner, _) => { - if let Self::Heading2(other_inner, _) = other { - if inner.len() != other_inner.len() { - return false; - } - inner - .iter() - .zip(other_inner.iter()) - .all(|(a, b)| a.eq_ignoring_span(b)) - } else { - false - } - } - Self::Heading3(inner, _) => { - if let Self::Heading3(other_inner, _) = other { - if inner.len() != other_inner.len() { - return false; - } - inner - .iter() - .zip(other_inner.iter()) - .all(|(a, b)| a.eq_ignoring_span(b)) - } else { - false - } - } - Self::Heading4(inner, _) => { - if let Self::Heading4(other_inner, _) = other { - if inner.len() != other_inner.len() { - return false; - } - inner - .iter() - .zip(other_inner.iter()) - .all(|(a, b)| a.eq_ignoring_span(b)) - } else { - false - } - } - Self::Heading5(inner, _) => { - if let Self::Heading5(other_inner, _) = other { - if inner.len() != other_inner.len() { - return false; - } - inner - .iter() - .zip(other_inner.iter()) - .all(|(a, b)| a.eq_ignoring_span(b)) - } else { - false - } - } - Self::Heading6(inner, _) => { - if let Self::Heading6(other_inner, _) = other { - if inner.len() != other_inner.len() { - return false; - } - inner - .iter() - .zip(other_inner.iter()) - .all(|(a, b)| a.eq_ignoring_span(b)) - } else { - false - } - } - Self::TextLine(inner, _) => { - if let Self::TextLine(other_inner, _) = other { - inner.eq_ignoring_span(other_inner) - } else { - false - } - } - Self::Table(inner, _) => { - if let Self::Table(other_inner, _) = other { - inner.eq_ignoring_span(other_inner) - } else { - false - } - } - Self::Newline(_) => { - matches!(other, Self::Newline(_)) - } - } - } -} diff --git a/src/angelmark/mod.rs b/src/angelmark/mod.rs deleted file mode 100644 index 902a43c..0000000 --- a/src/angelmark/mod.rs +++ /dev/null @@ -1,544 +0,0 @@ -#![deny(unsafe_code)] -#![warn(clippy::pedantic)] -#![warn(missing_docs)] - -/// Errors from AngelMark -mod error; -pub use error::Error; - -/// The AngelMark lexer -mod lexer; -use lexer::Rule; - -/// Abstracting a line -mod line; -pub use line::AngelmarkLine; - -/// Table structs and behaviours -mod table; -pub use table::{ - AngelmarkTable, AngelmarkTableAlignment, AngelmarkTableAlignmentCell, - AngelmarkTableAlignmentRow, AngelmarkTableCell, AngelmarkTableRow, -}; - -/// Text structs and behaviours -mod text; -pub use text::AngelmarkText; - -/// Traits for Angelmark -mod traits; -pub use traits::EqIgnoringSpan; - -use getset::Getters; -use pest::{Parser, Span, iterators::Pair}; -use regex::Regex; - -/// A parsed span with an owned clone of it's matched text -#[derive(Clone, Default, PartialEq, Eq, Hash, Getters)] -#[getset(get = "pub")] -pub struct OwnedSpan { - /// The original matched text - original: String, - /// The matched span - span: (usize, usize), -} - -impl std::fmt::Debug for OwnedSpan { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OwnedSpan").finish_non_exhaustive() - } -} - -impl From> for OwnedSpan { - fn from(value: Span<'_>) -> Self { - Self { - original: value.as_str().to_owned(), - span: (value.start(), value.end()), - } - } -} - -/// Parse an Angelmark markup string into a programmatically sensible interface. -/// -/// # Errors -/// -/// - [`Error::Parsing`] if the input markup couldn't be parsed. -#[allow(clippy::missing_panics_doc)] -pub fn parse_angelmark>(input: S) -> Result, Error> { - let markup_file = lexer::AngelmarkParser::parse(Rule::MarkupFile, input.as_ref()) - .map_err(Box::new)? - .next() - .unwrap(); - - let mut content = vec![]; - - for pair in markup_file.into_inner() { - let span = pair.as_span(); - match pair.as_rule() { - Rule::EOI => break, - Rule::Newline => { - if let Some(AngelmarkLine::Newline(_)) = content.last() { - } else { - content.push(AngelmarkLine::Newline(pair.as_span().into())); - } - } - - Rule::Heading1 => content.push(AngelmarkLine::Heading1( - pair.into_inner() - .filter(|pair| pair.as_rule() != Rule::Newline) - .map(|pair| parse_text_content(pair)) - .collect(), - span.into(), - )), - Rule::Heading2 => content.push(AngelmarkLine::Heading2( - pair.into_inner() - .filter(|pair| pair.as_rule() != Rule::Newline) - .map(|pair| parse_text_content(pair)) - .collect(), - span.into(), - )), - Rule::Heading3 => content.push(AngelmarkLine::Heading3( - pair.into_inner() - .filter(|pair| pair.as_rule() != Rule::Newline) - .map(|pair| parse_text_content(pair)) - .collect(), - span.into(), - )), - Rule::Heading4 => content.push(AngelmarkLine::Heading4( - pair.into_inner() - .filter(|pair| pair.as_rule() != Rule::Newline) - .map(|pair| parse_text_content(pair)) - .collect(), - span.into(), - )), - Rule::Heading5 => content.push(AngelmarkLine::Heading5( - pair.into_inner() - .filter(|pair| pair.as_rule() != Rule::Newline) - .map(|pair| parse_text_content(pair)) - .collect(), - span.into(), - )), - Rule::Heading6 => content.push(AngelmarkLine::Heading6( - pair.into_inner() - .filter(|pair| pair.as_rule() != Rule::Newline) - .map(|pair| parse_text_content(pair)) - .collect(), - span.into(), - )), - Rule::Table => content.push(AngelmarkLine::Table( - AngelmarkTable::from(pair), - span.into(), - )), - Rule::TextBold | Rule::TextItalic | Rule::TextMonospace | Rule::RawText => { - content.push(AngelmarkLine::TextLine( - parse_text_content(pair), - span.into(), - )); - } - - _ => unreachable!(), - } - } - - Ok(content) -} - -/// Parse rules into an [`AngelmarkText`] struct. -fn parse_text_content(pair: Pair) -> AngelmarkText { - assert!( - [ - Rule::TextBold, - Rule::TextItalic, - Rule::TextMonospace, - Rule::RawText, - ] - .contains(&pair.as_rule()) - ); - - let span = pair.as_span(); - match pair.as_rule() { - Rule::TextBold => AngelmarkText::Bold( - Box::new(parse_text_content(pair.into_inner().next().unwrap())), - span.into(), - ), - Rule::TextItalic => AngelmarkText::Italic( - Box::new(parse_text_content(pair.into_inner().next().unwrap())), - span.into(), - ), - Rule::TextMonospace => AngelmarkText::Monospace( - Box::new(parse_text_content(pair.into_inner().next().unwrap())), - span.into(), - ), - Rule::RawText => AngelmarkText::Raw(unescape_str(pair.as_str()), span.into()), - - _ => unreachable!(), - } -} - -/// Unescape a string, replacing \\x with x -fn unescape_str(s: &str) -> String { - let r = Regex::new(r"\\(.)").unwrap(); - r.replace_all(s, "$1").into_owned() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn empty_string_is_valid() { - parse_angelmark("").unwrap(); - } - - #[test] - fn single_line_with_no_newline_is_valid() { - parse_angelmark("test").unwrap(); - } - - #[test] - fn parse_test_document() { - let parsed_doc = parse_angelmark(include_str!("../../tests/angelmark.md")).unwrap(); - - let expected = vec![ - AngelmarkLine::Heading1( - vec![AngelmarkText::Raw( - "Heading 1".to_string(), - OwnedSpan::default(), - )], - OwnedSpan::default(), - ), - AngelmarkLine::Heading2( - vec![ - AngelmarkText::Bold( - Box::new(AngelmarkText::Raw( - "Heading".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - AngelmarkText::Raw(" 2".to_string(), OwnedSpan::default()), - ], - OwnedSpan::default(), - ), - AngelmarkLine::Heading3( - vec![ - AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "Heading".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - AngelmarkText::Raw(" 3".to_string(), OwnedSpan::default()), - ], - OwnedSpan::default(), - ), - AngelmarkLine::Heading4( - vec![AngelmarkText::Raw( - "Heading 4".to_string(), - OwnedSpan::default(), - )], - OwnedSpan::default(), - ), - AngelmarkLine::Heading5( - vec![AngelmarkText::Raw( - "Heading 5".to_string(), - OwnedSpan::default(), - )], - OwnedSpan::default(), - ), - AngelmarkLine::Heading6( - vec![AngelmarkText::Raw( - "Heading 6".to_string(), - OwnedSpan::default(), - )], - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Bold( - Box::new(AngelmarkText::Raw("Bold".to_string(), OwnedSpan::default())), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "Italic".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Bold( - Box::new(AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "Bold and italic".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "also italic".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Bold( - Box::new(AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "bold and italic".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Italic( - Box::new(AngelmarkText::Bold( - Box::new(AngelmarkText::Raw( - "bold and italic".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Raw("Formatting ".to_string(), OwnedSpan::default()), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Bold( - Box::new(AngelmarkText::Raw("in".to_string(), OwnedSpan::default())), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Raw(" a line ".to_string(), OwnedSpan::default()), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "as well".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Raw(" as ".to_string(), OwnedSpan::default()), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "on it's own".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Raw("!".to_string(), OwnedSpan::default()), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Monospace( - Box::new(AngelmarkText::Raw( - "monospace".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Raw( - r#"Something with_underlines_separating_it but that\ shouldn't be italicised!"# - .to_string(), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::Table( - AngelmarkTable { - rows: vec![ - AngelmarkTableRow { - cells: vec![ - AngelmarkTableCell { - content: vec![ - AngelmarkText::Bold( - Box::new(AngelmarkText::Raw( - "Test Case".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - AngelmarkText::Raw(" ".to_string(), OwnedSpan::default()), - ], - span: OwnedSpan::default(), - }, - AngelmarkTableCell { - content: vec![AngelmarkText::Raw( - " Objective ".to_string(), - OwnedSpan::default(), - )], - span: OwnedSpan::default(), - }, - AngelmarkTableCell { - content: vec![AngelmarkText::Raw( - " Expected Result".to_string(), - OwnedSpan::default(), - )], - span: OwnedSpan::default(), - }, - ], - span: OwnedSpan::default(), - }, - AngelmarkTableRow { - cells: vec![ - AngelmarkTableCell { - content: vec![ - AngelmarkText::Raw("TC".to_string(), OwnedSpan::default()), - AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "01".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - ], - span: OwnedSpan::default(), - }, - AngelmarkTableCell { - content: vec![], - span: OwnedSpan::default(), - }, - AngelmarkTableCell { - content: vec![AngelmarkText::Raw( - "DEF".to_string(), - OwnedSpan::default(), - )], - span: OwnedSpan::default(), - }, - ], - span: OwnedSpan::default(), - }, - AngelmarkTableRow { - cells: vec![ - AngelmarkTableCell { - content: vec![AngelmarkText::Italic( - Box::new(AngelmarkText::Raw( - "TC02".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - )], - span: OwnedSpan::default(), - }, - AngelmarkTableCell { - content: vec![AngelmarkText::Raw( - "HIJ".to_string(), - OwnedSpan::default(), - )], - span: OwnedSpan::default(), - }, - AngelmarkTableCell { - content: vec![AngelmarkText::Raw( - "KLM".to_string(), - OwnedSpan::default(), - )], - span: OwnedSpan::default(), - }, - ], - span: OwnedSpan::default(), - }, - ], - width: 3, - alignment: AngelmarkTableAlignmentRow { - column_alignments: vec![ - AngelmarkTableAlignmentCell { - alignment: AngelmarkTableAlignment::Right, - span: OwnedSpan::default(), - }, - AngelmarkTableAlignmentCell { - alignment: AngelmarkTableAlignment::Left, - span: OwnedSpan::default(), - }, - AngelmarkTableAlignmentCell { - alignment: AngelmarkTableAlignment::Center, - span: OwnedSpan::default(), - }, - ], - span: OwnedSpan::default(), - }, - span: OwnedSpan::default(), - }, - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - AngelmarkLine::TextLine( - AngelmarkText::Raw("Also ".to_string(), OwnedSpan::default()), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Monospace( - Box::new(AngelmarkText::Raw( - "monospace".to_string(), - OwnedSpan::default(), - )), - OwnedSpan::default(), - ), - OwnedSpan::default(), - ), - AngelmarkLine::TextLine( - AngelmarkText::Raw(" but in a line.".to_string(), OwnedSpan::default()), - OwnedSpan::default(), - ), - AngelmarkLine::Newline(OwnedSpan::default()), - ]; - - eprintln!("Parsed:"); - eprintln!("{parsed_doc:#?}"); - - eprintln!("\nExpected (ignoring spans):"); - eprintln!("{expected:#?}"); - - // Compare ignoring spans - let mut parsed_iter = parsed_doc.iter(); - for item in expected { - let parsed_item = parsed_iter.next().expect("not enough items were parsed"); - assert!(item.eq_ignoring_span(parsed_item)); - } - } -} diff --git a/src/angelmark/table.rs b/src/angelmark/table.rs deleted file mode 100644 index 68c1e2d..0000000 --- a/src/angelmark/table.rs +++ /dev/null @@ -1,277 +0,0 @@ -use std::cmp::Ordering; - -use getset::Getters; -use pest::iterators::Pair; - -use super::{AngelmarkText, EqIgnoringSpan, OwnedSpan, lexer::Rule}; - -/// A table -#[derive(Clone, Debug, PartialEq, Eq, Hash, Getters)] -#[getset(get = "pub")] -pub struct AngelmarkTable { - /// The rows of the table - pub(crate) rows: Vec, - /// Table width - pub(crate) width: usize, - /// The alignment row of the table - pub(crate) alignment: AngelmarkTableAlignmentRow, - /// The row's full span - pub(crate) span: OwnedSpan, -} - -impl AngelmarkTable { - /// Get the width and height of this table - #[must_use] - pub fn size(&self) -> (usize, usize) { - (self.width, self.rows.len()) - } - - /// Get the width in letters of a particular column - #[must_use] - pub fn column_width(&self, col: usize) -> usize { - let mut max_width = 0; - - for row in &self.rows { - if let Some(cell) = &row.cells.get(col) { - max_width = max_width.max(cell.content().iter().map(get_text_width).sum()); - } - } - - max_width - } -} - -/// Get the width of an [`AngelmarkText`] item -fn get_text_width(text: &AngelmarkText) -> usize { - match text { - AngelmarkText::Bold(text, _span) - | AngelmarkText::Italic(text, _span) - | AngelmarkText::Monospace(text, _span) => get_text_width(text), - AngelmarkText::Raw(text, _span) => text.len(), - } -} - -impl From> for AngelmarkTable { - fn from(pair: Pair<'_, Rule>) -> Self { - assert_eq!(pair.as_rule(), Rule::Table); - - let mut rows = vec![]; - let mut width = None; - let mut alignment_row = None; - - let span = pair.as_span(); - for pair in pair.into_inner() { - match pair.as_rule() { - Rule::TableRow => { - let mut row = AngelmarkTableRow::from(pair); - if let Some(target_width) = &width { - match row.cells().len().cmp(target_width) { - Ordering::Equal => (), - Ordering::Greater => { - tracing::warn!("More rows than expected width found!"); - } - Ordering::Less => { - while row.cells.len() < *target_width { - row.cells.push(AngelmarkTableCell { - content: vec![], - span: OwnedSpan::default(), - }); - } - } - } - } else { - width = Some(row.cells().len()); - } - rows.push(row); - } - Rule::TableAlignmentRow => { - alignment_row = Some(AngelmarkTableAlignmentRow::from(pair)); - } - _ => unreachable!(), - } - } - - Self { - rows, - width: width.unwrap(), - alignment: alignment_row.unwrap(), - span: span.into(), - } - } -} - -impl EqIgnoringSpan for AngelmarkTable { - fn eq_ignoring_span(&self, other: &Self) -> bool { - self.width == other.width - && self.alignment.eq_ignoring_span(&other.alignment) - && self - .rows - .iter() - .zip(&other.rows) - .all(|(a, b)| a.eq_ignoring_span(b)) - } -} - -/// A table row -#[derive(Clone, Debug, PartialEq, Eq, Hash, Getters)] -#[getset(get = "pub")] -pub struct AngelmarkTableRow { - /// The cells within the row - pub(crate) cells: Vec, - /// The span of the table row - pub(crate) span: OwnedSpan, -} - -impl From> for AngelmarkTableRow { - fn from(value: Pair<'_, Rule>) -> Self { - assert_eq!(value.as_rule(), Rule::TableRow); - - let span = value.as_span(); - let mut cells = vec![]; - for cell in value.into_inner() { - cells.push(AngelmarkTableCell::from(cell)); - } - - Self { - cells, - span: span.into(), - } - } -} - -impl EqIgnoringSpan for AngelmarkTableRow { - fn eq_ignoring_span(&self, other: &Self) -> bool { - self.cells.len() == other.cells.len() - && self - .cells - .iter() - .zip(&other.cells) - .all(|(a, b)| a.eq_ignoring_span(b)) - } -} - -/// A table alignment row -#[derive(Clone, Debug, PartialEq, Eq, Hash, Getters)] -#[getset(get = "pub")] -pub struct AngelmarkTableAlignmentRow { - /// The alignment cells of each column - pub(crate) column_alignments: Vec, - /// The span of the alignment row - pub(crate) span: OwnedSpan, -} - -impl From> for AngelmarkTableAlignmentRow { - fn from(value: Pair<'_, Rule>) -> Self { - assert_eq!(value.as_rule(), Rule::TableAlignmentRow); - - let span = value.as_span(); - let mut column_alignments = vec![]; - for cell in value.into_inner() { - column_alignments.push(AngelmarkTableAlignmentCell::from(cell)); - } - - Self { - column_alignments, - span: span.into(), - } - } -} - -impl EqIgnoringSpan for AngelmarkTableAlignmentRow { - fn eq_ignoring_span(&self, other: &Self) -> bool { - self.column_alignments.len() == other.column_alignments.len() - && self - .column_alignments - .iter() - .zip(&other.column_alignments) - .all(|(a, b)| a.eq_ignoring_span(b)) - } -} - -/// A table alignment cell -#[derive(Clone, Debug, PartialEq, Eq, Hash, Getters)] -#[getset(get = "pub")] -pub struct AngelmarkTableAlignmentCell { - /// The alignment specified by this alignment cell - pub(crate) alignment: AngelmarkTableAlignment, - /// The cell span - pub(crate) span: OwnedSpan, -} - -impl EqIgnoringSpan for AngelmarkTableAlignmentCell { - fn eq_ignoring_span(&self, other: &Self) -> bool { - self.alignment == other.alignment - } -} - -impl From> for AngelmarkTableAlignmentCell { - fn from(value: Pair<'_, Rule>) -> Self { - assert_eq!(value.as_rule(), Rule::TableAlignmentCell); - - let s = value.as_str(); - let alignment = if s.starts_with(':') && s.ends_with(':') { - AngelmarkTableAlignment::Center - } else if s.starts_with(':') { - AngelmarkTableAlignment::Left - } else if s.ends_with(':') { - AngelmarkTableAlignment::Right - } else { - // (default) - AngelmarkTableAlignment::Left - }; - - Self { - alignment, - span: value.as_span().into(), - } - } -} - -/// A specified alignment -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum AngelmarkTableAlignment { - /// Align left - Left, - /// Align center - Center, - /// Align right - Right, -} - -/// A table cell -#[derive(Clone, Debug, PartialEq, Eq, Hash, Getters)] -#[getset(get = "pub")] -pub struct AngelmarkTableCell { - /// The content of this cell - pub(crate) content: Vec, - /// The span for this cell - pub(crate) span: OwnedSpan, -} - -impl From> for AngelmarkTableCell { - fn from(value: Pair<'_, Rule>) -> Self { - assert_eq!(value.as_rule(), Rule::TableCell); - - let span = value.as_span(); - let mut content = vec![]; - for pair in value.into_inner() { - content.push(super::parse_text_content(pair)); - } - - Self { - content, - span: span.into(), - } - } -} - -impl EqIgnoringSpan for AngelmarkTableCell { - fn eq_ignoring_span(&self, other: &Self) -> bool { - self.content.len() == other.content.len() - && self - .content - .iter() - .zip(&other.content) - .all(|(a, b)| a.eq_ignoring_span(b)) - } -} diff --git a/src/angelmark/text.rs b/src/angelmark/text.rs deleted file mode 100644 index c073d19..0000000 --- a/src/angelmark/text.rs +++ /dev/null @@ -1,63 +0,0 @@ -use super::{EqIgnoringSpan, OwnedSpan}; - -/// Textual content -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum AngelmarkText { - /// Raw text - Raw(String, OwnedSpan), - /// Bold content - Bold(Box, OwnedSpan), - /// Italicised content - Italic(Box, OwnedSpan), - /// Monospace content - Monospace(Box, OwnedSpan), -} - -impl AngelmarkText { - /// Get the span from this text - #[must_use] - pub fn span(&self) -> &OwnedSpan { - match self { - Self::Raw(_, span) - | Self::Bold(_, span) - | Self::Italic(_, span) - | Self::Monospace(_, span) => span, - } - } -} - -impl EqIgnoringSpan for AngelmarkText { - /// Compare two [`AngelmarkText`] instances, ignoring their span. - fn eq_ignoring_span(&self, other: &Self) -> bool { - match self { - Self::Raw(inner, _) => { - if let Self::Raw(other_inner, _) = other { - inner == other_inner - } else { - false - } - } - Self::Bold(inner, _) => { - if let Self::Bold(other_inner, _) = other { - inner.eq_ignoring_span(other_inner) - } else { - false - } - } - Self::Italic(inner, _) => { - if let Self::Italic(other_inner, _) = other { - inner.eq_ignoring_span(other_inner) - } else { - false - } - } - Self::Monospace(inner, _) => { - if let Self::Monospace(other_inner, _) = other { - inner.eq_ignoring_span(other_inner) - } else { - false - } - } - } - } -} diff --git a/src/angelmark/traits.rs b/src/angelmark/traits.rs deleted file mode 100644 index 63bd2d3..0000000 --- a/src/angelmark/traits.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Check equality between two parsed structs ignoring the internal spans. -pub trait EqIgnoringSpan { - /// Check equality between two parsed structs ignoring the internal spans. - #[must_use] - fn eq_ignoring_span(&self, other: &Self) -> bool; -} diff --git a/src/exporters.rs b/src/exporters.rs index f32eedc..f3c3084 100644 --- a/src/exporters.rs +++ b/src/exporters.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use uuid::Uuid; -use crate::{EvidencePackage, Result}; +use crate::prelude::{EvidencePackage, Result}; /// Exporter for Excel files. #[cfg(feature = "exporter-excel")] diff --git a/src/exporters/excel.rs b/src/exporters/excel.rs index 0d1fbd0..51f4102 100644 --- a/src/exporters/excel.rs +++ b/src/exporters/excel.rs @@ -1,8 +1,8 @@ -use crate::angelmark::{AngelmarkLine, AngelmarkTableAlignment, AngelmarkText, parse_angelmark}; -use rust_xlsxwriter::{Format, FormatAlign, FormatBorder, Image, Note, Workbook, Worksheet}; +use markdown::{ParseOptions, mdast::Node}; +use rust_xlsxwriter::{Format, FormatBorder, Image, Note, Workbook, Worksheet}; use uuid::Uuid; -use crate::{EvidenceKind, EvidencePackage, TestCase, TestCasePassStatus}; +use crate::prelude::{Error, EvidencePackage, Result as EVPResult, TestCase, TestCasePassStatus}; use super::Exporter; @@ -23,26 +23,25 @@ impl Exporter for ExcelExporter { &mut self, package: &mut EvidencePackage, path: std::path::PathBuf, - ) -> crate::Result<()> { + ) -> EVPResult<()> { let mut workbook = Workbook::new(); workbook.read_only_recommended(); create_metadata_sheet(workbook.add_worksheet(), package) - .map_err(crate::Error::OtherExportError)?; + .map_err(Error::OtherExportError)?; - create_summary_sheet(workbook.add_worksheet(), package) - .map_err(crate::Error::OtherExportError)?; + create_summary_sheet(workbook.add_worksheet(), package).map_err(Error::OtherExportError)?; let test_cases: Vec<&TestCase> = package.test_case_iter()?.collect(); for test_case in test_cases { let worksheet = workbook.add_worksheet(); create_test_case_sheet(worksheet, package.clone(), test_case) - .map_err(crate::Error::OtherExportError)?; + .map_err(Error::OtherExportError)?; } workbook .save(path) - .map_err(|e| crate::Error::OtherExportError(e.into()))?; + .map_err(|e| Error::OtherExportError(e.into()))?; Ok(()) } @@ -52,21 +51,19 @@ impl Exporter for ExcelExporter { package: &mut EvidencePackage, case: Uuid, path: std::path::PathBuf, - ) -> crate::Result<()> { + ) -> EVPResult<()> { let mut workbook = Workbook::new(); let worksheet = workbook.add_worksheet(); let case = package .test_case(case)? - .ok_or(crate::Error::OtherExportError( - "Test case not found!".into(), - ))?; + .ok_or(Error::OtherExportError("Test case not found!".into()))?; create_test_case_sheet(worksheet, package.clone(), case) - .map_err(crate::Error::OtherExportError)?; + .map_err(Error::OtherExportError)?; workbook .save(path) - .map_err(|e| crate::Error::OtherExportError(e.into()))?; + .map_err(|e| Error::OtherExportError(e.into()))?; Ok(()) } @@ -135,7 +132,7 @@ fn create_summary_sheet( worksheet.write_string_with_format(row, 2, "Executed At", &bold_bordered)?; worksheet.write_string_with_format(row, 3, "Status", &bold_bordered)?; let mut custom_keys = vec![]; - if let Some(fields) = package.metadata().custom_test_case_metadata() { + if let Some(fields) = package.metadata().custom_metadata() { let mut fields = fields.iter().collect::>(); fields.sort_by(|(_, a), (_, b)| a.cmp(b)); for (idx, (key, field)) in fields.iter().enumerate() { @@ -171,10 +168,10 @@ fn create_summary_sheet( for (idx, key) in custom_keys.iter().enumerate() { let col = u16::try_from(4 + idx)?; worksheet.write_string_with_format(row, col, "", &bordered)?; - if let Some(custom) = test_case.metadata().custom() { - if let Some(data) = custom.get(key) { - worksheet.write_string_with_format(row, col, data, &bordered)?; - } + if let Some(custom) = test_case.metadata().custom() + && let Some(data) = custom.get(key) + { + worksheet.write_string_with_format(row, col, data, &bordered)?; } } row += 1; @@ -232,7 +229,7 @@ fn create_test_case_sheet( for (key, value) in fields { let field = package .metadata() - .custom_test_case_metadata() + .custom_metadata() .as_ref() // SAFETY: guanteed by EVP spec .unwrap() @@ -252,8 +249,8 @@ fn create_test_case_sheet( row += 1; } - match evidence.kind() { - EvidenceKind::Text => { + match evidence.kind().as_str() { + "text/plain" => { let data = evidence.value().get_data(&mut package)?; let text = String::from_utf8_lossy(data.as_slice()); for line in text.lines() { @@ -261,200 +258,24 @@ fn create_test_case_sheet( row += 1; } } - EvidenceKind::RichText => { + "text/markdown" => { let data = evidence.value().get_data(&mut package)?; let text = String::from_utf8_lossy(data.as_slice()); - if let Ok(mut rich_text) = parse_angelmark(&text) { - if !matches!(rich_text.last(), Some(AngelmarkLine::Newline(_))) { - rich_text.push(AngelmarkLine::Newline( - crate::angelmark::OwnedSpan::default(), - )); - } - let mut line_buffer: Vec<(Format, String)> = vec![]; - for line in rich_text { - match line { - AngelmarkLine::Newline(_span) => { - if !line_buffer.is_empty() { - worksheet.write_rich_string( - row, - 1, - &line_buffer - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(), - )?; - line_buffer.clear(); - } - row += 1; - } - AngelmarkLine::Heading1(angelmark, _span) => { - let fragments = angelmark - .iter() - .map(|text| { - angelmark_to_excel( - text, - Format::default().set_font_size(32), - ) - }) - .collect::>(); - let fragments = fragments - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(); - if !fragments.is_empty() { - worksheet.write_rich_string(row, 1, &fragments)?; - worksheet.set_row_height(row, 36)?; - } - row += 1; - } - AngelmarkLine::Heading2(angelmark, _span) => { - let fragments = angelmark - .iter() - .map(|text| { - angelmark_to_excel( - text, - Format::default().set_font_size(28), - ) - }) - .collect::>(); - let fragments = fragments - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(); - if !fragments.is_empty() { - worksheet.write_rich_string(row, 1, &fragments)?; - worksheet.set_row_height(row, 32)?; - } - row += 1; - } - AngelmarkLine::Heading3(angelmark, _span) => { - let fragments = angelmark - .iter() - .map(|text| { - angelmark_to_excel( - text, - Format::default().set_font_size(24), - ) - }) - .collect::>(); - let fragments = fragments - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(); - if !fragments.is_empty() { - worksheet.write_rich_string(row, 1, &fragments)?; - worksheet.set_row_height(row, 28)?; - } - row += 1; - } - AngelmarkLine::Heading4(angelmark, _span) => { - let fragments = angelmark - .iter() - .map(|text| { - angelmark_to_excel( - text, - Format::default().set_font_size(18), - ) - }) - .collect::>(); - let fragments = fragments - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(); - if !fragments.is_empty() { - worksheet.write_rich_string(row, 1, &fragments)?; - worksheet.set_row_height(row, 22)?; - } - row += 1; - } - AngelmarkLine::Heading5(angelmark, _span) => { - let fragments = angelmark - .iter() - .map(|text| { - angelmark_to_excel( - text, - Format::default().set_font_size(16), - ) - }) - .collect::>(); - let fragments = fragments - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(); - if !fragments.is_empty() { - worksheet.write_rich_string(row, 1, &fragments)?; - worksheet.set_row_height(row, 20)?; - } - row += 1; - } - AngelmarkLine::Heading6(angelmark, _span) => { - let fragments = angelmark - .iter() - .map(|text| { - angelmark_to_excel( - text, - Format::default().set_font_size(14), - ) - }) - .collect::>(); - let fragments = fragments - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(); - if !fragments.is_empty() { - worksheet.write_rich_string(row, 1, &fragments)?; - worksheet.set_row_height(row, 18)?; - } - row += 1; - } - AngelmarkLine::TextLine(angelmark, _span) => { - line_buffer.push(angelmark_to_excel(&angelmark, Format::default())); - } - AngelmarkLine::Table(table, _span) => { - for table_row in table.rows() { - for (col, cell) in table_row.cells().iter().enumerate() { - let fragments = cell - .content() - .iter() - .map(|text| { - angelmark_to_excel( - text, - Format::default().set_font_size(14), - ) - }) - .collect::>(); - let c = col as u16 + 1; - let align = - table.alignment().column_alignments()[col].alignment(); - worksheet.set_cell_format( - row, - c, - &Format::default().set_align(match align { - AngelmarkTableAlignment::Left => FormatAlign::Left, - AngelmarkTableAlignment::Center => { - FormatAlign::Center - } - AngelmarkTableAlignment::Right => { - FormatAlign::Right - } - }), - )?; - if !fragments.is_empty() { - worksheet.write_rich_string( - row, - c, - &fragments - .iter() - .map(|(f, s)| (f, s.as_str())) - .collect::>(), - )?; - } - } - row += 1; - } - } - } + if let Ok(ast) = markdown::to_mdast(&text, &ParseOptions::default()) { + let excel_data = markdown_to_excel(ast, &Format::default()); + for line in excel_data { + let line = line + .iter() + .map(|(a, b)| (a, b.as_str())) + .collect::>(); + worksheet.write_rich_string_with_format( + row, + 1, + line.iter().as_slice(), + &Format::default(), + )?; + row += 1; } } else { for line in text.lines() { @@ -468,18 +289,7 @@ fn create_test_case_sheet( } } } - EvidenceKind::Image => { - let data = evidence.value().get_data(&mut package)?; - let image = Image::new_from_buffer(data.as_slice())?; - worksheet.insert_image(row, 1, &image)?; - - // Calculate row offset - let height_in = image.height() / image.height_dpi(); - let row_units_per_in = 4.87; - let num_rows_to_skip = (height_in * row_units_per_in).ceil() as u32; - row += num_rows_to_skip; - } - EvidenceKind::Http => { + "text/vnd.angel.http-data" => { worksheet.write_string_with_format(row, 1, "HTTP Request", &bold)?; row += 1; let data = evidence.value().get_data(&mut package)?; @@ -489,32 +299,44 @@ fn create_test_case_sheet( row += 1; } } - EvidenceKind::File => { - let data = evidence.value().get_data(&mut package)?; - let text = String::from_utf8_lossy(data.as_slice()); + mime => { + if mime.starts_with("image/") { + let data = evidence.value().get_data(&mut package)?; + let image: Image = Image::new_from_buffer(data.as_slice())?; + worksheet.insert_image(row, 1, &image)?; + + // Calculate row offset + let height_in = image.height() / image.height_dpi(); + let row_units_per_in = 4.87; + let num_rows_to_skip = (height_in * row_units_per_in).ceil() as u32; + row += num_rows_to_skip; + } else { + let data = evidence.value().get_data(&mut package)?; + let text = String::from_utf8_lossy(data.as_slice()); - if let Some(filename) = evidence.original_filename() { - worksheet.write_string(row, 1, filename)?; - row += 1; - } + if let Some(filename) = evidence.original_filename() { + worksheet.write_string(row, 1, filename)?; + row += 1; + } - // Check if plaintext ASCII - let mut is_printable = true; - for c in text.chars() { - if !c.is_ascii() { - is_printable = false; - break; + // Check if plaintext ASCII + let mut is_printable = true; + for c in text.chars() { + if !c.is_ascii() { + is_printable = false; + break; + } } - } - if is_printable { - for line in text.lines() { - worksheet.write_string_with_format(row, 1, line, &file_data)?; + if is_printable { + for line in text.lines() { + worksheet.write_string_with_format(row, 1, line, &file_data)?; + row += 1; + } + } else { + worksheet.write_string_with_format(row, 1, "binary file data", &italic)?; row += 1; } - } else { - worksheet.write_string_with_format(row, 1, "binary file data", &italic)?; - row += 1; } } } @@ -525,14 +347,69 @@ fn create_test_case_sheet( Ok(()) } -/// Convert Angelmark to Excel format data -fn angelmark_to_excel(angelmark: &AngelmarkText, format: Format) -> (Format, String) { - match angelmark { - AngelmarkText::Raw(txt, _span) => (format, txt.clone()), - AngelmarkText::Bold(content, _span) => angelmark_to_excel(content, format.set_bold()), - AngelmarkText::Italic(content, _span) => angelmark_to_excel(content, format.set_italic()), - AngelmarkText::Monospace(content, _span) => { - angelmark_to_excel(content, format.set_font_name("Courier New")) +/// Convert Markdown AST to an array of lines, each an array of line +/// elements defining a format and associated text. +fn markdown_to_excel(node: Node, format: &Format) -> Vec> { + match node { + Node::Root(root) => root + .children + .into_iter() + .flat_map(|c| markdown_to_excel(c, format)) + .collect(), + Node::Paragraph(para) => para + .children + .into_iter() + .flat_map(|c| markdown_to_excel(c, format)) + .collect(), + Node::List(list) => list + .children + .into_iter() + .flat_map(|c| markdown_to_excel(c, format)) + .collect(), + Node::ListItem(item) => { + let mut item = item + .children + .into_iter() + .flat_map(|c| markdown_to_excel(c, format)) + .collect::>(); + if let Some(fir) = item.first_mut() { + fir.insert(0, (format.clone(), "• ".to_string())); + } + item + } + Node::Heading(hdg) => { + let font_sizes = [32, 28, 24, 18, 16, 14]; + let size = font_sizes.get(usize::from(hdg.depth)).unwrap_or(&14); + hdg.children + .into_iter() + .flat_map(|c| markdown_to_excel(c, &format.clone().set_font_size(*size))) + .collect() + } + Node::Code(code) => { + vec![vec![( + format.clone().set_font_name("Courier New"), + code.value, + )]] + } + Node::Text(text) => { + vec![vec![(format.clone(), text.value)]] + } + Node::InlineCode(code) => { + vec![vec![( + format.clone().set_font_name("Courier New"), + code.value, + )]] } + Node::Strong(strong) => strong + .children + .into_iter() + .flat_map(|c| markdown_to_excel(c, &format.clone().set_bold())) + .collect(), + Node::Emphasis(emph) => emph + .children + .into_iter() + .flat_map(|c| markdown_to_excel(c, &format.clone().set_italic())) + .collect(), + _ => vec![], } } diff --git a/src/exporters/html.rs b/src/exporters/html.rs index f7a040a..78ae82c 100644 --- a/src/exporters/html.rs +++ b/src/exporters/html.rs @@ -1,12 +1,14 @@ use std::fmt::Write; use std::fs; -use crate::angelmark::{AngelmarkLine, AngelmarkTableAlignment, AngelmarkText, parse_angelmark}; use base64::Engine; use build_html::{Html, HtmlContainer, HtmlElement, HtmlPage, HtmlTag}; use uuid::Uuid; -use crate::{EvidenceData, EvidenceKind, EvidencePackage, MediaFile, TestCase, TestCasePassStatus}; +use crate::prelude::{ + Error, EvidenceData, EvidencePackage, MediaFile, Result as EVPResult, TestCase, + TestCasePassStatus, +}; use super::Exporter; @@ -27,7 +29,7 @@ impl Exporter for HtmlExporter { &mut self, package: &mut EvidencePackage, path: std::path::PathBuf, - ) -> crate::Result<()> { + ) -> EVPResult<()> { let mut page = HtmlPage::new() .with_title(html_escape::encode_text(package.metadata().title())) .with_style(include_str!("html.css")) @@ -113,7 +115,7 @@ impl Exporter for HtmlExporter { tab_container.add_html(tab_elem); let elem = create_test_case_div(package.clone(), test_case) - .map_err(crate::Error::OtherExportError)? + .map_err(Error::OtherExportError)? .with_attribute("data-tab-index", idx) .with_attribute( "class", @@ -145,7 +147,7 @@ impl Exporter for HtmlExporter { package: &mut EvidencePackage, case: Uuid, path: std::path::PathBuf, - ) -> crate::Result<()> { + ) -> EVPResult<()> { let mut page = HtmlPage::new() .with_title(html_escape::encode_text(package.metadata().title())) .with_style(include_str!("html.css")) @@ -153,11 +155,8 @@ impl Exporter for HtmlExporter { let case = package .test_case(case)? - .ok_or(crate::Error::OtherExportError( - "Test case not found!".into(), - ))?; - let elem = - create_test_case_div(package.clone(), case).map_err(crate::Error::OtherExportError)?; + .ok_or(Error::OtherExportError("Test case not found!".into()))?; + let elem = create_test_case_div(package.clone(), case).map_err(Error::OtherExportError)?; page.add_html(elem); fs::write(path, page.to_html_string())?; @@ -205,7 +204,7 @@ fn create_test_case_div( for (key, value) in fields { let field = package .metadata() - .custom_test_case_metadata() + .custom_metadata() .as_ref() // SAFETY: guanteed by EVP spec .unwrap() @@ -237,8 +236,8 @@ fn create_test_case_div( ); } - match evidence.kind() { - EvidenceKind::Text => { + match evidence.kind().as_str() { + "text/plain" => { let data = evidence.value().get_data(&mut package)?; let text = String::from_utf8_lossy(data.as_slice()); for line in text.lines() { @@ -248,128 +247,12 @@ fn create_test_case_div( ); } } - EvidenceKind::RichText => { + "text/markdown" => { let data = evidence.value().get_data(&mut package)?; let text = String::from_utf8_lossy(data.as_slice()); - if let Ok(rich_text) = parse_angelmark(&text) { - for line in rich_text { - match line { - AngelmarkLine::Newline(_span) => { - elem.add_html(HtmlElement::new(HtmlTag::LineBreak)); - } - AngelmarkLine::Heading1(angelmark_texts, _span) => { - let mut h = HtmlElement::new(HtmlTag::Heading1); - for angelmark in angelmark_texts { - h.add_html(angelmark_to_html( - &angelmark, - HtmlElement::new(HtmlTag::Span), - )); - } - elem.add_html(h); - } - AngelmarkLine::Heading2(angelmark_texts, _span) => { - let mut h = HtmlElement::new(HtmlTag::Heading2); - for angelmark in angelmark_texts { - h.add_html(angelmark_to_html( - &angelmark, - HtmlElement::new(HtmlTag::Span), - )); - } - elem.add_html(h); - } - AngelmarkLine::Heading3(angelmark_texts, _span) => { - let mut h = HtmlElement::new(HtmlTag::Heading3); - for angelmark in angelmark_texts { - h.add_html(angelmark_to_html( - &angelmark, - HtmlElement::new(HtmlTag::Span), - )); - } - elem.add_html(h); - } - AngelmarkLine::Heading4(angelmark_texts, _span) => { - let mut h = HtmlElement::new(HtmlTag::Heading4); - for angelmark in angelmark_texts { - h.add_html(angelmark_to_html( - &angelmark, - HtmlElement::new(HtmlTag::Span), - )); - } - elem.add_html(h); - } - AngelmarkLine::Heading5(angelmark_texts, _span) => { - let mut h = HtmlElement::new(HtmlTag::Heading5); - for angelmark in angelmark_texts { - h.add_html(angelmark_to_html( - &angelmark, - HtmlElement::new(HtmlTag::Span), - )); - } - elem.add_html(h); - } - AngelmarkLine::Heading6(angelmark_texts, _span) => { - let mut h = HtmlElement::new(HtmlTag::Heading6); - for angelmark in angelmark_texts { - h.add_html(angelmark_to_html( - &angelmark, - HtmlElement::new(HtmlTag::Span), - )); - } - elem.add_html(h); - } - AngelmarkLine::TextLine(angelmark, _span) => elem.add_html( - angelmark_to_html(&angelmark, HtmlElement::new(HtmlTag::Span)), - ), - AngelmarkLine::Table(table, _span) => { - let mut t = HtmlElement::new(HtmlTag::Table); - for row in table.rows() { - let mut r = HtmlElement::new(HtmlTag::TableRow); - for (col, cell) in row.cells().iter().enumerate() { - let align = - table.alignment().column_alignments()[col].alignment(); - let mut d = HtmlElement::new(HtmlTag::TableCell) - .with_attribute( - "style", - format!( - "text-align:{}", - match align { - AngelmarkTableAlignment::Left => "left", - AngelmarkTableAlignment::Center => "center", - AngelmarkTableAlignment::Right => "right", - } - ), - ); - for angelmark in cell.content() { - d.add_html(angelmark_to_html( - angelmark, - HtmlElement::new(HtmlTag::Span), - )); - } - r.add_html(d); - } - t.add_html(r); - } - elem.add_html(t); - } - } - } - elem.add_html(HtmlElement::new(HtmlTag::LineBreak)); - } else { - elem.add_html(HtmlElement::new(HtmlTag::CodeText).with_preformatted(text)); - } - } - EvidenceKind::Image => { - let data = evidence.value().get_data(&mut package)?; - let media = MediaFile::from(data); - if let Some(mime) = media.mime_type() { - let data = base64::prelude::BASE64_STANDARD_NO_PAD.encode(media.data()); - elem.add_html( - HtmlElement::new(HtmlTag::Image) - .with_attribute("src", format!("data:{mime};base64,{data}")), - ); - } + elem.add_raw(markdown::to_html(&text)); } - EvidenceKind::Http => { + "text/vnd.angel.http-data" => { let data = evidence.value().get_data(&mut package)?; let data = String::from_utf8_lossy(data.as_slice()); let data_parts = data @@ -406,75 +289,56 @@ fn create_test_case_div( ), ); } - EvidenceKind::File => { - let data = evidence.value().get_data(&mut package)?; - let data = base64::prelude::BASE64_STANDARD_NO_PAD.encode(data); - let mime = if let EvidenceData::Media { hash } = evidence.value() { - if let Some(media) = package.get_media(hash).ok().flatten() { - if let Some(mime) = media.mime_type() { - mime.to_string() + mime => { + if mime.starts_with("image/") { + let data = evidence.value().get_data(&mut package)?; + let media = MediaFile::from(data); + if let Some(mime) = media.mime_type() { + let data = base64::prelude::BASE64_STANDARD_NO_PAD.encode(media.data()); + elem.add_html( + HtmlElement::new(HtmlTag::Image) + .with_attribute("src", format!("data:{mime};base64,{data}")), + ); + } + } else { + let data = evidence.value().get_data(&mut package)?; + let data = base64::prelude::BASE64_STANDARD_NO_PAD.encode(data); + let mime = if let EvidenceData::Media { hash } = evidence.value() { + if let Some(media) = package.get_media(hash).ok().flatten() { + if let Some(mime) = media.mime_type() { + mime.to_string() + } else { + "application/octet-stream".to_string() + } } else { "application/octet-stream".to_string() } } else { "application/octet-stream".to_string() - } - } else { - "application/octet-stream".to_string() - }; + }; - elem.add_html( - HtmlElement::new(HtmlTag::Div).with_html( - HtmlElement::new(HtmlTag::Link) - .with_attribute("href", format!("data:{mime};base64,{data}")) - .with_attribute( - "download", - evidence - .original_filename() - .clone() - .unwrap_or(String::new()), - ) - .with_raw(&if let Some(filename) = evidence.original_filename() { - filename.clone() - } else { - "Unnamed file".to_string() - }), - ), - ); + elem.add_html( + HtmlElement::new(HtmlTag::Div).with_html( + HtmlElement::new(HtmlTag::Link) + .with_attribute("href", format!("data:{mime};base64,{data}")) + .with_attribute( + "download", + evidence + .original_filename() + .clone() + .unwrap_or(String::new()), + ) + .with_raw(&if let Some(filename) = evidence.original_filename() { + filename.clone() + } else { + "Unnamed file".to_string() + }), + ), + ); + } } } } Ok(elem) } - -/// Convert Angelmark to HTML elements -fn angelmark_to_html(angelmark: &AngelmarkText, mut elem: HtmlElement) -> HtmlElement { - match angelmark { - AngelmarkText::Raw(txt, _span) => elem.with_raw(html_escape::encode_text(txt)), - AngelmarkText::Bold(content, _span) => { - if let Some((_k, v)) = elem.attributes.iter_mut().find(|(k, _v)| k == "class") { - v.push_str(" richtext-bold"); - } else { - elem.add_attribute("class", "richtext-bold"); - } - angelmark_to_html(content, elem) - } - AngelmarkText::Italic(content, _span) => { - if let Some((_k, v)) = elem.attributes.iter_mut().find(|(k, _v)| k == "class") { - v.push_str(" richtext-italic"); - } else { - elem.add_attribute("class", "richtext-italic"); - } - angelmark_to_html(content, elem) - } - AngelmarkText::Monospace(content, _span) => { - if let Some((_k, v)) = elem.attributes.iter_mut().find(|(k, _v)| k == "class") { - v.push_str(" richtext-monospace"); - } else { - elem.add_attribute("class", "richtext-monospace"); - } - angelmark_to_html(content, elem) - } - } -} diff --git a/src/exporters/zip_of_files.rs b/src/exporters/zip_of_files.rs index 15e8867..8df2bd6 100644 --- a/src/exporters/zip_of_files.rs +++ b/src/exporters/zip_of_files.rs @@ -6,12 +6,20 @@ use std::{ use thiserror::Error; use uuid::Uuid; -use zip::{ZipWriter, write::SimpleFileOptions}; +use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions}; -use crate::{EvidenceKind, EvidencePackage, TestCase}; +use crate::prelude::{Error, EvidenceData, EvidencePackage, Result as EVPResult, TestCase}; use super::Exporter; +/// Prefixes to MIME types that are treated as not-files +const NOT_FILE_MIME_TYPES: &[&str] = &[ + "image/", + "text/plain", + "text/markdown", + "text/vnd.angel.http-data", +]; + /// An exporter to an ZIP of the files in the package. #[derive(Default)] pub struct ZipOfFilesExporter; @@ -37,18 +45,18 @@ impl Exporter for ZipOfFilesExporter { &mut self, package: &mut EvidencePackage, path: std::path::PathBuf, - ) -> crate::Result<()> { + ) -> EVPResult<()> { fn safely_add_cases_to_zip( mut zip: ZipWriter>, package: &mut EvidencePackage, - ) -> crate::Result<()> { + ) -> EVPResult<()> { for test_case in package.test_case_iter()? { add_test_case_to_zip(&mut zip, package.clone(), test_case) - .map_err(crate::Error::OtherExportError)?; + .map_err(Error::OtherExportError)?; } zip.finish() - .map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; + .map_err(|e| Error::OtherExportError(Box::new(e)))?; Ok(()) } @@ -61,13 +69,13 @@ impl Exporter for ZipOfFilesExporter { } } if !has_files { - return Err(crate::Error::OtherExportError(Box::new( + return Err(Error::OtherExportError(Box::new( ZipOfFilesError::NoFilesToExport, ))); } let zip = ZipWriter::new(BufWriter::new( - fs::File::create(&path).map_err(|e| crate::Error::OtherExportError(Box::new(e)))?, + fs::File::create(&path).map_err(|e| Error::OtherExportError(Box::new(e)))?, )); if let Err(e) = safely_add_cases_to_zip(zip, package) { // Delete file if exists @@ -84,36 +92,33 @@ impl Exporter for ZipOfFilesExporter { package: &mut EvidencePackage, case: Uuid, path: std::path::PathBuf, - ) -> crate::Result<()> { + ) -> EVPResult<()> { fn inner( mut zip: ZipWriter>, package: &mut EvidencePackage, case: &TestCase, - ) -> crate::Result<()> { + ) -> EVPResult<()> { add_test_case_to_zip(&mut zip, package.clone(), case) - .map_err(crate::Error::OtherExportError)?; + .map_err(Error::OtherExportError)?; zip.finish() - .map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; + .map_err(|e| Error::OtherExportError(Box::new(e)))?; Ok(()) } let case = package .test_case(case)? - .ok_or(crate::Error::OtherExportError( - "Test case not found!".into(), - ))? + .ok_or(Error::OtherExportError("Test case not found!".into()))? .clone(); if !check_has_files(&case) { - return Err(crate::Error::OtherExportError(Box::new( + return Err(Error::OtherExportError(Box::new( ZipOfFilesError::NoFilesToExport, ))); } - let file = - fs::File::create(&path).map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; + let file = fs::File::create(&path).map_err(|e| Error::OtherExportError(Box::new(e)))?; let zip = ZipWriter::new(BufWriter::new(file)); if let Err(e) = inner(zip, package, &case) { // Delete file if exists @@ -129,7 +134,14 @@ impl Exporter for ZipOfFilesExporter { /// Check is this test case contains any file evidence fn check_has_files(test_case: &TestCase) -> bool { for ev in test_case.evidence() { - if let EvidenceKind::File = ev.kind() { + let mut is_file = true; + for kind in NOT_FILE_MIME_TYPES { + if ev.kind().starts_with(kind) { + is_file = false; + break; + } + } + if is_file { return true; } } @@ -146,9 +158,15 @@ fn add_test_case_to_zip( let mut filename_count = HashMap::new(); for ev in test_case.evidence() { - if let EvidenceKind::File = ev.kind() - && let Some(filename) = ev.original_filename() - { + let mut is_file = true; + for kind in NOT_FILE_MIME_TYPES { + if ev.kind().starts_with(kind) { + is_file = false; + break; + } + } + + if is_file && let Some(filename) = ev.original_filename() { filename_count.insert(filename, filename_count.get(filename).unwrap_or(&0) + 1); } } @@ -157,12 +175,20 @@ fn add_test_case_to_zip( // Write evidence for evidence in test_case.evidence() { - if let EvidenceKind::File = evidence.kind() { + let mut is_file = true; + for kind in NOT_FILE_MIME_TYPES { + if evidence.kind().starts_with(kind) { + is_file = false; + break; + } + } + + if is_file { let data = evidence.value().get_data(&mut package)?; let name = if let Some(filename) = evidence.original_filename() { filename.clone() - } else if let crate::EvidenceData::Media { hash } = evidence.value() { + } else if let EvidenceData::Media { hash } = evidence.value() { hash.clone() } else { unnamed_counter += 1; @@ -186,12 +212,11 @@ fn add_test_case_to_zip( // Add to ZIP file zip.start_file( format!("{}/{disambiguator}{name}", test_case.metadata().title()), - SimpleFileOptions::default(), + SimpleFileOptions::default().compression_method(CompressionMethod::Deflated), ) - .map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; + .map_err(|e| Error::OtherExportError(Box::new(e)))?; let mut data_cursor = Cursor::new(data); - io::copy(&mut data_cursor, zip) - .map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; + io::copy(&mut data_cursor, zip).map_err(|e| Error::OtherExportError(Box::new(e)))?; } } diff --git a/src/lib.rs b/src/lib.rs index dc2cd8b..af320c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,28 +6,42 @@ #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_sign_loss)] -//! # `EvidenceAngel` +//! # `evp` //! -//! `EvidenceAngel` is a new tool in the Angel-suite to collect test evidence -//! from both manual and automated testing. +//! `evp` is a library that reads and produces Evidence Packages, as +//! defined by Internet Draft [draft-hopkins-evp-spec](https://hpkns.uk/evp) -/// Angelmark is a Markdown-like markup language for EVP Rich Text. -/// -/// Eventually, this will be replaced with another solution that -/// supports Markdown more completely. -pub mod angelmark; +/// Exporters allow packages and test cases to be exported to different file formats. +pub mod exporters; /// Locking file mod lock_file; /// The types of data in a package -mod package; -pub use package::{ - Author, CustomMetadataField, Evidence, EvidenceData, EvidenceKind, EvidencePackage, MediaFile, - Metadata, TestCase, TestCaseMetadata, TestCasePassStatus, -}; +pub mod package; /// The results of this crate -mod result; -pub use result::{Error, Result}; -/// Exporters allow packages and test cases to be exported to different file formats. -pub mod exporters; +pub mod result; /// Open a ZIP file in a fashion that allows it to be switched between reading and writing. mod zip_read_writer; + +/// Import useful items from the `evp` package in one go. +pub mod prelude { + pub use super::package::{ + Author, CustomMetadataField, Evidence, EvidenceData, EvidencePackage, MediaFile, Metadata, + TestCase, TestCaseMetadata, TestCasePassStatus, + }; + pub use super::result::{Error, Result}; + /// Items relating to creating and parsing attestations + pub mod attesting { + /// The attestation JWS type + pub type Attestation = biscuit::jws::Compact, biscuit::Empty>; + pub use biscuit::errors::*; + pub use biscuit::jwa::SignatureAlgorithm as Algorithm; + /// The attestation JWS header + pub type Header = biscuit::jws::Header; + pub use biscuit::jws::{RegisteredHeader, Secret}; + /// The attestation JWS set for validation + pub type JWKSet = biscuit::jwk::JWKSet; + /// The attestation JWS for validation + pub type JWK = biscuit::jwk::JWK; + pub use biscuit::jwk::*; + } +} diff --git a/src/package.rs b/src/package.rs index 5dabf33..cb358ff 100644 --- a/src/package.rs +++ b/src/package.rs @@ -1,4 +1,5 @@ use std::{ + cell::RefCell, collections::HashMap, fmt, io::{self, BufReader, Read, Write}, @@ -10,9 +11,9 @@ use getset::{Getters, MutGetters}; use serde::{Deserialize, Serialize}; use test_cases::TESTCASE_SCHEMA; use uuid::Uuid; -use zip::{result::ZipError, write::SimpleFileOptions}; +use zip::{CompressionMethod, result::ZipError, write::SimpleFileOptions}; -use crate::{Result, result::Error, zip_read_writer::ZipReaderWriter}; +use crate::{prelude::*, zip_read_writer::ZipReaderWriter}; /// Package manifests mod manifest; @@ -24,9 +25,7 @@ pub use media::MediaFile; /// Test cases from packages mod test_cases; -pub use test_cases::{ - Evidence, EvidenceData, EvidenceKind, TestCase, TestCaseMetadata, TestCasePassStatus, -}; +pub use test_cases::{Evidence, EvidenceData, TestCase, TestCaseMetadata, TestCasePassStatus}; /// The URL for $schema for manifest.json const MANIFEST_SCHEMA_LOCATION: &str = @@ -41,10 +40,10 @@ const MANIFEST_SCHEMA_V1: &str = include_str!("../schemas/manifest.1.schema.json pub struct EvidencePackage { /// The internal ZIP file. This will never be `None`, as long as it has been correctly parsed. #[serde(skip)] - zip: ZipReaderWriter, + zip: RefCell, /// The actual media data from this package #[serde(skip)] - media_data: HashMap, + media_data: RefCell>, /// The actual test data from this package #[serde(skip)] test_case_data: HashMap, @@ -69,7 +68,7 @@ impl Clone for EvidencePackage { fn clone(&self) -> Self { Self { zip: self.zip.clone(), - media_data: HashMap::new(), + media_data: RefCell::new(HashMap::new()), test_case_data: self.test_case_data.clone(), extra_fields: HashMap::new(), @@ -104,7 +103,11 @@ impl EvidencePackage { clippy::missing_panics_doc, reason = "panics have been statically validated to never occur" )] - pub fn new(path: PathBuf, title: String, authors: Vec) -> Result { + pub fn new, A: Clone + Into>( + path: PathBuf, + title: S, + authors: &[A], + ) -> Result { Self::new_with_description(path, title, None, authors) } @@ -119,58 +122,63 @@ impl EvidencePackage { clippy::missing_panics_doc, reason = "panics have been statically validated to never occur" )] - pub fn new_with_description( + pub fn new_with_description, A: Clone + Into>( path: PathBuf, - title: String, + title: S, description: Option, - authors: Vec, + authors: &[A], ) -> Result { // Create manifest data. - let mut manifest = Self { - zip: ZipReaderWriter::new(path)?, - media_data: HashMap::new(), + let manifest = Self { + zip: RefCell::new(ZipReaderWriter::new(path)?), + media_data: RefCell::new(HashMap::new()), test_case_data: HashMap::new(), schema: Some(MANIFEST_SCHEMA_LOCATION.to_string()), media: vec![], test_cases: vec![], metadata: Metadata { - title, + title: title.into(), description, - authors, - custom_test_case_metadata: None, + authors: authors.iter().cloned().map(Into::into).collect(), + custom_metadata: None, extra_fields: HashMap::new(), }, extra_fields: HashMap::new(), }; let manifest_clone = manifest.clone_serde(); - // Create ZIP file - let (_, zip) = manifest.zip.as_writer()?; - let options = SimpleFileOptions::default(); - - // Create empty structure. - zip.add_directory("media", options)?; - zip.add_directory("testcases", options)?; + { + // Create ZIP file + let mut zip_ref = manifest.zip.borrow_mut(); + let (_, zip) = zip_ref.as_writer()?; + let options = + SimpleFileOptions::default().compression_method(CompressionMethod::Deflated); + + // Create empty structure. + zip.add_directory("media", options)?; + zip.add_directory("test_cases", options)?; + + let manifest_data = + serde_json::to_string(&manifest_clone).map_err(Error::FailedToCreatePackage)?; + if !jsonschema::is_valid( + &serde_json::from_str(MANIFEST_SCHEMA).expect("Schema is validated statically"), + &serde_json::from_str(&manifest_data).expect("JSON just generated, shouldn't fail"), + ) { + return Err(Error::ManifestSchemaValidationFailed); + } - let manifest_data = - serde_json::to_string(&manifest_clone).map_err(Error::FailedToCreatePackage)?; - if !jsonschema::is_valid( - &serde_json::from_str(MANIFEST_SCHEMA).expect("Schema is validated statically"), - &serde_json::from_str(&manifest_data).expect("JSON just generated, shouldn't fail"), - ) { - return Err(Error::ManifestSchemaValidationFailed); + // Write ZIP file. + zip.start_file("manifest.json", options)?; + zip.write_all(manifest_data.as_bytes())?; + zip_ref.conclude_write()?; } - // Write ZIP file. - zip.start_file("manifest.json", options)?; - zip.write_all(manifest_data.as_bytes())?; - manifest.zip.conclude_write()?; - Ok(manifest) } - /// Save the package to disk. + /// Save the package to disk. This also removes any media that is + /// unreferenced. /// /// # Panics /// @@ -190,14 +198,15 @@ impl EvidencePackage { { // IMPORTANT! // This needs to be here to load the archive in read mode first, so that media can be migrated over. - let _reader = self.zip.as_reader()?; + let _reader = self.zip.borrow_mut().as_reader()?; } - let (mut maybe_old_archive, zip) = self.zip.as_writer()?; - let options = SimpleFileOptions::default(); + let mut zip_ref = self.zip.borrow_mut(); + let (mut maybe_old_archive, zip) = zip_ref.as_writer()?; + let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated); // Create empty structure. zip.add_directory("media", options)?; - zip.add_directory("testcases", options)?; + zip.add_directory("test_cases", options)?; tracing::trace!("Current media cache: {:?}", self.media_data); @@ -216,14 +225,15 @@ impl EvidencePackage { let data = serde_json::to_string(data) .map_err(crate::result::Error::FailedToSaveTestCase)?; + tracing::trace!("Generated test case manifest:\n{data}"); if !jsonschema::is_valid( &serde_json::from_str(TESTCASE_SCHEMA).expect("Schema is validated statically"), &serde_json::from_str(&data).expect("JSON just generated, shouldn't fail"), ) { - let _ = self.zip.interrupt_write(); + let _ = zip_ref.interrupt_write(); return Err(Error::TestCaseSchemaValidationFailed); } - zip.start_file(format!("testcases/{id}.json"), options)?; + zip.start_file(format!("test_cases/{id}.json"), options)?; zip.write_all(data.as_bytes())?; } } @@ -235,22 +245,26 @@ impl EvidencePackage { .media .retain(|entry| media_used.contains(&entry.sha256_checksum())); - // Scrub media map of unreferenced entries - self.media_data - .retain(|hash, _val| media_used.contains(&hash)); - clone - .media_data - .retain(|hash, _val| media_used.contains(&hash)); + { + // Scrub media map of unreferenced entries + self.media_data + .borrow_mut() + .retain(|hash, _val| media_used.contains(&hash)); + clone + .media_data + .borrow_mut() + .retain(|hash, _val| media_used.contains(&hash)); + } // Save media to package, either sourcing it from memory if present, or from the previous package. tracing::debug!("Media entries: {:?}", self.media); for entry in &self.media { let hash = entry.sha256_checksum(); zip.start_file(format!("media/{hash}"), options)?; - if self.media_data.contains_key(hash) { + if self.media_data.borrow().contains_key(hash) { // If in memory, write from there tracing::trace!("Writing from cache {hash}"); - zip.write_all(self.media_data.get(hash).unwrap().data())?; + zip.write_all(self.media_data.borrow().get(hash).unwrap().data())?; } else { // Otherwise pull from previous package. // Consider moving this to not load entire file on move. @@ -277,16 +291,17 @@ impl EvidencePackage { // Write manifest. This has to be done last to ensure media is scrubbed as needed. let manifest_data = serde_json::to_string(&clone).map_err(Error::FailedToCreatePackage)?; + tracing::trace!("Generated package manifest:\n{manifest_data}"); if !jsonschema::is_valid( &serde_json::from_str(MANIFEST_SCHEMA).expect("Schema is validated statically"), &serde_json::from_str(&manifest_data).expect("JSON just generated, shouldn't fail"), ) { - let _ = self.zip.interrupt_write(); + let _ = zip_ref.interrupt_write(); return Err(Error::ManifestSchemaValidationFailed); } zip.start_file("manifest.json", options)?; zip.write_all(manifest_data.as_bytes())?; - self.zip.conclude_write()?; + zip_ref.conclude_write()?; Ok(()) } @@ -347,7 +362,7 @@ impl EvidencePackage { for test_case in &evidence_package.test_cases { let id = test_case.id(); let data = zip - .by_name(&format!("testcases/{id}.json")) + .by_name(&format!("test_cases/{id}.json")) .map_err(|_| Error::CorruptEvidencePackage(format!("missing test case {id}")))?; let test_case_data = { let mut buf_test_case = BufReader::new(data); @@ -363,7 +378,7 @@ impl EvidencePackage { .map_err(|_| Error::TestCaseSchemaValidationFailed)?, ) { // Read as version 1 - tracing::debug!("Test case {id} opened as version 1"); + tracing::debug!("Test case {id} opened as version 2"); let mut test_case: TestCase = serde_json::from_str(&test_case_data) .map_err(|e| Error::InvalidTestCase(e, *id))?; test_case.set_id(*id); @@ -374,15 +389,15 @@ impl EvidencePackage { } } - evidence_package.zip = zip_rw; + evidence_package.zip = RefCell::new(zip_rw); Ok(evidence_package) } /// Clone fields that will be serialized by serde fn clone_serde(&self) -> Self { Self { - zip: ZipReaderWriter::default(), - media_data: HashMap::new(), + zip: RefCell::new(ZipReaderWriter::default()), + media_data: RefCell::new(HashMap::new()), test_case_data: HashMap::new(), schema: Some(MANIFEST_SCHEMA_LOCATION.to_string()), @@ -447,6 +462,88 @@ impl EvidencePackage { Ok(()) } + /// Add an attestation for a test case + /// + /// # Errors + /// + /// Returns an [`Error::Attesting`] if an attestation failed to be + /// parsed. + pub fn attest_test_case( + &mut self, + test_case: Uuid, + secret: &attesting::Secret, + header: attesting::RegisteredHeader, + ) -> Result<()> { + let payload = self + .test_case(test_case)? + .ok_or(Error::DoesntExist(test_case))? + .attestation_payload() + .into_bytes(); + let jws = attesting::Attestation::new_decoded( + attesting::Header::from_registered_header(header), + payload, + ); + let jws_enc = jws.encode(secret)?.unwrap_encoded(); + for tc in &mut self.test_cases { + if *tc.id() == test_case { + #[allow(clippy::to_string_in_format_args, reason = "clippy gets this wrong")] + tc.attestations.push(jws_enc.to_string()); + } + } + Ok(()) + } + + /// Remove the attestation with the given index + /// + /// # Errors + /// + /// Returns an [`Error::Attesting`] if an attestation failed to be + /// parsed. + pub fn remove_attestation(&mut self, test_case: Uuid, index: usize) -> Result<()> { + for tc in &mut self.test_cases { + if *tc.id() == test_case { + tc.attestations.remove(index); + return Ok(()); + } + } + Err(Error::DoesntExist(test_case)) + } + + /// Returns the attestations associated with a test case. It is up + /// to you to perform validation on these! + /// + /// Attestations are returned with a boolean flag whih indicates if + /// the attestation payload matches the current state of the test + /// case. + /// + /// # Errors + /// + /// Returns an [`Error::Attesting`] if an attestation failed to be + /// parsed. + #[allow(clippy::missing_panics_doc, reason = "manually verified cannot panic")] + pub fn test_case_attestations( + &self, + test_case: Uuid, + ) -> Result> { + let payload = self + .test_case(test_case)? + .ok_or(Error::DoesntExist(test_case))? + .attestation_payload() + .into_bytes(); + for tc in &self.test_cases { + if *tc.id() == test_case { + let attestations = tc + .attestations() + .iter() + .map(|a| attesting::Attestation::new_encoded(a)) + .map(|a| (a.clone(), payload == a.unverified_payload().unwrap())) + .collect::>(); + return Ok(attestations); + } + } + Err(Error::DoesntExist(test_case)) + } + /// Create a new test case. /// /// # Errors @@ -474,7 +571,7 @@ impl EvidencePackage { where S: Into, { - let new_id = uuid::Uuid::new_v4(); + let new_id = Uuid::now_v7(); // Create new manifest entry self.test_cases.push(TestCaseManifestEntry::new(new_id)); @@ -498,7 +595,7 @@ impl EvidencePackage { .cloned() .ok_or(Error::DoesntExist(case_id_to_duplicate))?; let mut new_case = case.clone(); - let new_id = Uuid::new_v4(); + let new_id = Uuid::now_v7(); new_case.set_id(new_id); // Create new manifest entry @@ -586,7 +683,7 @@ impl EvidencePackage { /// - [`Error::Io`] if the evp couldn't be read at all. /// - [`Error::Zip`] if the evp file couldn't be read correctly. #[allow(clippy::missing_panics_doc)] - pub fn add_media(&mut self, media_file: MediaFile) -> Result<&MediaFile> { + pub fn add_media(&mut self, media_file: MediaFile) -> Result { let hash = media_file.hash(); if !self @@ -600,7 +697,9 @@ impl EvidencePackage { // Insert data and return reference tracing::trace!("New media cache entry: {hash}"); - self.media_data.insert(hash.clone(), media_file); + self.media_data + .borrow_mut() + .insert(hash.clone(), media_file); } Ok(self.get_media(&hash)?.unwrap()) @@ -617,20 +716,21 @@ impl EvidencePackage { /// - [`Error::Io`] if the evp couldn't be read at all. /// - [`Error::Zip`] if the evp file couldn't be read correctly. #[allow(clippy::missing_panics_doc)] - pub fn get_media(&mut self, hash: S) -> Result> + pub fn get_media(&self, hash: S) -> Result> where S: Into, { let hash = hash.into(); // Check in-memory cache - if self.media_data.contains_key(&hash) { + if self.media_data.borrow().contains_key(&hash) { tracing::debug!("{hash} found in cache."); - return Ok(self.media_data.get(&hash)); + return Ok(self.media_data.borrow().get(&hash).cloned()); } // Read from ZIP file - let zip = self.zip.as_reader()?; + let mut zip = self.zip.borrow_mut(); + let zip = zip.as_reader()?; let res = zip.by_name(&format!("media/{}", hash.clone())); match res { Ok(file) => { @@ -645,10 +745,10 @@ impl EvidencePackage { // Add to in-memory cache let media: MediaFile = buf.into(); tracing::trace!("New media cache entry: {hash}"); - self.media_data.insert(hash.clone(), media); + self.media_data.borrow_mut().insert(hash.clone(), media); // Return cached version - Ok(Some(self.media_data.get(&hash).unwrap())) + Ok(Some(self.media_data.borrow().get(&hash).cloned().unwrap())) } Err(ZipError::FileNotFound) => { tracing::warn!("{hash} not found in package!"); diff --git a/src/package/manifest.rs b/src/package/manifest.rs index 2b89132..745dbb7 100644 --- a/src/package/manifest.rs +++ b/src/package/manifest.rs @@ -4,6 +4,8 @@ use uuid::Uuid; use std::{collections::HashMap, fmt}; +use crate::prelude::*; + /// [`EvidencePackage`](super::EvidencePackage) metadata. #[derive(Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)] #[getset(get = "pub", set = "pub")] @@ -21,11 +23,12 @@ pub struct Metadata { /// Custom metadata fields for test cases #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "custom_test_case_metadata")] #[allow( clippy::struct_field_names, reason = "This field refers to the name of it's subtype" )] - pub(super) custom_test_case_metadata: Option>, + pub(super) custom_metadata: Option>, /// Extra fields that this implementation doesn't understand. #[get = "pub"] @@ -37,11 +40,11 @@ impl Metadata { /// Get a mutable reference to custom metadata fields for test cases #[allow(clippy::missing_panics_doc, reason = "safety is explained inline")] pub fn custom_test_case_metadata_mut(&mut self) -> &mut HashMap { - if self.custom_test_case_metadata.is_none() { - self.custom_test_case_metadata = Some(HashMap::new()); + if self.custom_metadata.is_none() { + self.custom_metadata = Some(HashMap::new()); } // SAFETY: just initialised if wasn't previously - self.custom_test_case_metadata.as_mut().unwrap() + self.custom_metadata.as_mut().unwrap() } /// Create a new custom metadata field @@ -61,7 +64,7 @@ impl Metadata { } } - let new_id = id.unwrap_or_else(|| Uuid::new_v4().to_string()); + let new_id = id.unwrap_or_else(|| Uuid::now_v7().to_string()); let field = CustomMetadataField { name, description, @@ -119,6 +122,16 @@ impl fmt::Display for Author { } } +impl, S2: Into> From<(S1, S2)> for Author { + fn from((name, email): (S1, S2)) -> Self { + Self { + name: name.into(), + email: Some(email.into()), + extra_fields: HashMap::new(), + } + } +} + /// A custom metadata field for [`TestCase`](super::test_cases::TestCase)s. #[derive(Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize, PartialEq, Eq)] #[getset(get = "pub", get_mut = "pub", set = "pub")] @@ -169,8 +182,8 @@ pub(super) struct MediaFileManifestEntry { extra_fields: HashMap, } -impl From<&crate::MediaFile> for MediaFileManifestEntry { - fn from(value: &crate::MediaFile) -> Self { +impl From<&MediaFile> for MediaFileManifestEntry { + fn from(value: &MediaFile) -> Self { Self { sha256_checksum: value.hash(), mime_type: value @@ -189,7 +202,10 @@ impl From<&crate::MediaFile> for MediaFileManifestEntry { pub(super) struct TestCaseManifestEntry { /// A string to reference the test case internally. Usually a UUID. #[serde(alias = "name")] // Compatibility with previous pre-RFC field name `name`. - id: Uuid, + pub(super) id: Uuid, + + /// Attestations over the associated test case + pub(super) attestations: Vec, /// Extra fields that this implementation doesn't understand. #[get = "pub"] @@ -202,6 +218,7 @@ impl TestCaseManifestEntry { pub(super) fn new(id: Uuid) -> Self { Self { id, + attestations: vec![], extra_fields: HashMap::new(), } } diff --git a/src/package/test_cases.rs b/src/package/test_cases.rs index 23ebdcd..3259f2c 100644 --- a/src/package/test_cases.rs +++ b/src/package/test_cases.rs @@ -9,11 +9,13 @@ use serde::{ }; use uuid::Uuid; +use crate::prelude::{Error, EvidencePackage, Result as EVPResult}; + /// The URL for $schema in the test case manifests const TESTCASE_SCHEMA_LOCATION: &str = - "https://evidenceangel-schemas.hpkns.uk/testcase.1.schema.json"; + "https://evidenceangel-schemas.hpkns.uk/testcase.2.schema.json"; /// The schema itself for test case manifests (version 2) -pub(crate) const TESTCASE_SCHEMA: &str = include_str!("../../schemas/testcase.1.schema.json"); +pub(crate) const TESTCASE_SCHEMA: &str = include_str!("../../schemas/testcase.2.schema.json"); /// A test case stored within an [`EvidencePackage`](super::EvidencePackage). #[derive(Clone, Debug, Serialize, Deserialize, Getters, MutGetters, Setters)] @@ -63,6 +65,17 @@ impl TestCase { pub(super) fn update_schema(&mut self) { self.schema = Some(TESTCASE_SCHEMA_LOCATION.to_string()); } + + /// Generate the data needed for signing an attestation of this test + /// case, in it's current state. + #[allow( + clippy::missing_panics_doc, + reason = "safety is statically checked by me" + )] + #[must_use] + pub fn attestation_payload(&self) -> String { + sha256::digest(serde_json_canonicalizer::to_string(&self).unwrap()) + } } /// The metadata of a [`TestCase`]. @@ -112,8 +125,8 @@ pub enum TestCasePassStatus { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, MutGetters, Setters)] #[getset(get = "pub")] pub struct Evidence { - /// The kind of this evidence. - kind: EvidenceKind, + /// The MIME type of this evidence. + kind: String, /// The data contained within this piece of evidence. #[getset(get_mut = "pub", set = "pub")] @@ -137,11 +150,12 @@ pub struct Evidence { } impl Evidence { - /// Create a new evidence object. + /// Create a new evidence object. `kind` must be a valid MIME type + /// for the data. #[must_use] - pub fn new(kind: EvidenceKind, value: EvidenceData) -> Self { + pub fn new>(kind: S, value: EvidenceData) -> Self { Self { - kind, + kind: kind.into(), value, caption: None, original_filename: None, @@ -186,21 +200,6 @@ impl Evidence { } } -/// Kinds of [`Evidence`]. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum EvidenceKind { - /// A text entry. - Text, - /// A rich text (`AngelMark`) entry. - RichText, - /// An image. - Image, - /// An attached file. - File, - /// An HTTP request and response. - Http, -} - /// Data in a piece of [`Evidence`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum EvidenceData { @@ -214,9 +213,9 @@ pub enum EvidenceData { /// The raw data which will be encoded as base64 automatically when saved. data: Vec, }, - /// A [`MediaFile`](crate::MediaFile). This is useful for large files that would be unreasonable to store as text or base64. + /// A [`MediaFile`](crate::prelude::MediaFile). This is useful for large files that would be unreasonable to store as text or base64. Media { - /// The hash of the [`MediaFile`](crate::MediaFile) that should be referred to. Note that you are responsible for adding + /// The hash of the [`MediaFile`](crate::prelude::MediaFile) that should be referred to. Note that you are responsible for adding /// a media file of the appropriate type to the package. hash: String, }, @@ -227,15 +226,15 @@ impl EvidenceData { /// /// # Errors /// - /// - [`crate::Error::MediaMissing`] if the media referred to by the requested data is missing from the package. - pub fn get_data(&self, package: &mut crate::EvidencePackage) -> crate::Result> { + /// - [`Error::MediaMissing`] if the media referred to by the requested data is missing from the package. + pub fn get_data(&self, package: &mut EvidencePackage) -> EVPResult> { match self { Self::Text { content } => Ok(content.clone().into_bytes()), Self::Base64 { data } => Ok(data.clone()), Self::Media { hash } => package .get_media(hash)? .map(|mf| mf.data().clone()) - .ok_or(crate::Error::MediaMissing(hash.clone())), + .ok_or(Error::MediaMissing(hash.clone())), } } } diff --git a/src/result.rs b/src/result.rs index d3df935..274016d 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,7 +1,7 @@ use thiserror::Error; use uuid::Uuid; -/// An error raised by `EvidenceAngel`. +/// An error raised by the `evp` library. #[derive(Debug, Error)] pub enum Error { /// You are trying to perform an operation without a lock on the package. @@ -18,6 +18,10 @@ pub enum Error { #[error("Package error: {0}")] Zip(#[from] zip::result::ZipError), + /// An error from creating or verifying an attestation + #[error("Error attesting: {0}")] + Attesting(#[from] crate::prelude::attesting::Error), + /// The package is corrupt. See the contained string for more details. #[error("The evidence package is corrupt ({0}).")] CorruptEvidencePackage(String), diff --git a/src/zip_read_writer.rs b/src/zip_read_writer.rs index 8ac3097..30fa55f 100644 --- a/src/zip_read_writer.rs +++ b/src/zip_read_writer.rs @@ -7,6 +7,7 @@ use std::{ use zip::{ZipArchive, ZipWriter}; use crate::lock_file::LockFile; +use crate::prelude::{Error, Result}; /// A convenient type which can read and write to a ZIP file and cleanly switch between the two modes. /// @@ -52,7 +53,7 @@ impl fmt::Debug for ZipReaderWriter { impl ZipReaderWriter { /// Create a new [`ZipReaderWriter`] instance. - pub fn new(path: path::PathBuf) -> crate::Result { + pub fn new(path: path::PathBuf) -> Result { let mut o = Self { path: Some(path), ..Default::default() @@ -63,21 +64,21 @@ impl ZipReaderWriter { /// Validate that the currently held lock is still locking the /// package. - fn validate_lock(&mut self) -> crate::Result<()> { + fn validate_lock(&mut self) -> Result<()> { if let Some(lock_file) = self.lock_file.as_mut() { lock_file.ensure_still_locked().map_err(|e| { tracing::error!("The lock was lost! {e}"); - crate::Error::LockNotObtained + Error::LockNotObtained }) } else { - Err(crate::Error::LockNotObtained) + Err(Error::LockNotObtained) } } /// Update the locking file for this [`ZipReaderWriter`]. This will /// either obtain it (if a path is set), drop it (if a path isn't /// set), or will return a [`crate::Error::LockNotObtained`] error. - fn update_lock_file(&mut self) -> crate::Result<()> { + fn update_lock_file(&mut self) -> Result<()> { if let Some(path) = &self.path { let mut lock_path = path.clone(); // SAFETY: only a file can be specified here @@ -92,7 +93,7 @@ impl ZipReaderWriter { )); self.lock_file = Some(LockFile::new(lock_path).map_err(|e| { tracing::error!("Locking error: {e}"); - crate::Error::LockNotObtained + Error::LockNotObtained })?); } else { self.lock_file = None; @@ -101,7 +102,7 @@ impl ZipReaderWriter { } /// Get this [`ZipReaderWriter`] instance in read mode. - pub fn as_reader(&mut self) -> crate::Result<&mut ZipArchive>> { + pub fn as_reader(&mut self) -> Result<&mut ZipArchive>> { if self.reader.is_none() { // Close writer tracing::debug!("Closing writer"); @@ -122,7 +123,7 @@ impl ZipReaderWriter { #[allow(clippy::type_complexity)] pub fn as_writer( &mut self, - ) -> crate::Result<( + ) -> Result<( Option<&mut ZipArchive>>, &mut ZipWriter>, )> { @@ -148,7 +149,7 @@ impl ZipReaderWriter { } /// Conclude writing to the ZIP file and reset for reading or writing again. - pub fn conclude_write(&mut self) -> crate::Result<()> { + pub fn conclude_write(&mut self) -> Result<()> { self.validate_lock()?; if self.writer.is_some() { // Close write @@ -181,7 +182,7 @@ impl ZipReaderWriter { } /// Interrupt a write early, concluding the write and removing the temporary file. - pub fn interrupt_write(&mut self) -> crate::Result<()> { + pub fn interrupt_write(&mut self) -> Result<()> { self.validate_lock()?; if self.writer.is_some() { // Close write diff --git a/tests/angelmark.md b/tests/angelmark.md deleted file mode 100644 index 0bc7ba5..0000000 --- a/tests/angelmark.md +++ /dev/null @@ -1,31 +0,0 @@ -# Heading 1 -## **Heading** 2 -### _Heading_ 3 -#### Heading 4 -##### Heading 5 -###### Heading 6 - -**Bold** - -*Italic* - -***Bold and italic*** - -_also italic_ - -**_bold and italic_** - -_**bold and italic**_ - -Formatting **in** a line *as well* as _on it's own_! - -`monospace` - -Something with\_underlines\_separating\_it but that\\ shouldn't be italicised! - -**Test Case** | Objective | Expected Result -|--------------:|:--|:-:| -|TC_01_||DEF| -|_TC02_|HIJ|KLM| - -Also `monospace` but in a line. diff --git a/tests/ecdsa_private_key.p8 b/tests/ecdsa_private_key.p8 new file mode 100644 index 0000000..bd7d6a8 Binary files /dev/null and b/tests/ecdsa_private_key.p8 differ diff --git a/tests/image.jpg b/tests/image.jpg new file mode 100644 index 0000000..a759b56 Binary files /dev/null and b/tests/image.jpg differ diff --git a/tests/pour.mp3 b/tests/pour.mp3 new file mode 100644 index 0000000..3cbbae3 Binary files /dev/null and b/tests/pour.mp3 differ diff --git a/tests/test.md b/tests/test.md new file mode 100644 index 0000000..d8eb375 --- /dev/null +++ b/tests/test.md @@ -0,0 +1,631 @@ +# Headers + +``` +# h1 Heading 8-) +## h2 Heading +### h3 Heading +#### h4 Heading +##### h5 Heading +###### h6 Heading + +Alternatively, for H1 and H2, an underline-ish style: + +Alt-H1 +====== + +Alt-H2 +------ +``` + +# h1 Heading 8-) +## h2 Heading +### h3 Heading +#### h4 Heading +##### h5 Heading +###### h6 Heading + +Alternatively, for H1 and H2, an underline-ish style: + +Alt-H1 +====== + +Alt-H2 +------ + +------ + +# Emphasis + +``` +Emphasis, aka italics, with *asterisks* or _underscores_. + +Strong emphasis, aka bold, with **asterisks** or __underscores__. + +Combined emphasis with **asterisks and _underscores_**. + +Strikethrough uses two tildes. ~~Scratch this.~~ + +**This is bold text** + +__This is bold text__ + +*This is italic text* + +_This is italic text_ + +~~Strikethrough~~ +``` + +Emphasis, aka italics, with *asterisks* or _underscores_. + +Strong emphasis, aka bold, with **asterisks** or __underscores__. + +Combined emphasis with **asterisks and _underscores_**. + +Strikethrough uses two tildes. ~~Scratch this.~~ + +**This is bold text** + +__This is bold text__ + +*This is italic text* + +_This is italic text_ + +~~Strikethrough~~ + +------ + +# Lists + +``` +1. First ordered list item +2. Another item +⋅⋅* Unordered sub-list. +1. Actual numbers don't matter, just that it's a number +⋅⋅1. Ordered sub-list +4. And another item. + +⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + +⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅ +⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅ +⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.) + +* Unordered list can use asterisks +- Or minuses ++ Or pluses + +1. Make my changes + 1. Fix bug + 2. Improve formatting + - Make the headings bigger +2. Push my commits to GitHub +3. Open a pull request + * Describe my changes + * Mention all the members of my team + * Ask for feedback + ++ Create a list by starting a line with `+`, `-`, or `*` ++ Sub-lists are made by indenting 2 spaces: + - Marker character change forces new list start: + * Ac tristique libero volutpat at + + Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit ++ Very easy! +``` + +1. First ordered list item +2. Another item +⋅⋅* Unordered sub-list. +1. Actual numbers don't matter, just that it's a number +⋅⋅1. Ordered sub-list +4. And another item. + +⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + +⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅ +⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅ +⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.) + +* Unordered list can use asterisks +- Or minuses ++ Or pluses + +1. Make my changes + 1. Fix bug + 2. Improve formatting + - Make the headings bigger +2. Push my commits to GitHub +3. Open a pull request + * Describe my changes + * Mention all the members of my team + * Ask for feedback + ++ Create a list by starting a line with `+`, `-`, or `*` ++ Sub-lists are made by indenting 2 spaces: + - Marker character change forces new list start: + * Ac tristique libero volutpat at + + Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit ++ Very easy! + +------ + +# Task lists + +``` +- [x] Finish my changes +- [ ] Push my commits to GitHub +- [ ] Open a pull request +- [x] @mentions, #refs, [links](), **formatting**, and tags supported +- [x] list syntax required (any unordered or ordered list supported) +- [x] this is a complete item +- [ ] this is an incomplete item +``` + +- [x] Finish my changes +- [ ] Push my commits to GitHub +- [ ] Open a pull request +- [x] @mentions, #refs, [links](), **formatting**, and tags supported +- [x] list syntax required (any unordered or ordered list supported) +- [ ] this is a complete item +- [ ] this is an incomplete item + +------ + +# Ignoring Markdown formatting + +You can tell GitHub to ignore (or escape) Markdown formatting by using \ before the Markdown character. + +``` +Let's rename \*our-new-project\* to \*our-old-project\*. +``` + +Let's rename \*our-new-project\* to \*our-old-project\*. + +------ + +# Links + +``` +[I'm an inline-style link](https://www.google.com) + +[I'm an inline-style link with title](https://www.google.com "Google's Homepage") + +[I'm a reference-style link][Arbitrary case-insensitive reference text] + +[I'm a relative reference to a repository file](../blob/master/LICENSE) + +[You can use numbers for reference-style link definitions][1] + +Or leave it empty and use the [link text itself]. + +URLs and URLs in angle brackets will automatically get turned into links. +http://www.example.com or and sometimes +example.com (but not on Github, for example). + +Some text to show that the reference links can follow later. + +[arbitrary case-insensitive reference text]: https://www.mozilla.org +[1]: http://slashdot.org +[link text itself]: http://www.reddit.com +``` + +[I'm an inline-style link](https://www.google.com) + +[I'm an inline-style link with title](https://www.google.com "Google's Homepage") + +[I'm a reference-style link][Arbitrary case-insensitive reference text] + +[I'm a relative reference to a repository file](../blob/master/LICENSE) + +[You can use numbers for reference-style link definitions][1] + +Or leave it empty and use the [link text itself]. + +URLs and URLs in angle brackets will automatically get turned into links. +http://www.example.com or and sometimes +example.com (but not on Github, for example). + +Some text to show that the reference links can follow later. + +[arbitrary case-insensitive reference text]: https://www.mozilla.org +[1]: http://slashdot.org +[link text itself]: http://www.reddit.com + +------ + +# Images + +``` +Here's our logo (hover to see the title text): + +Inline-style: +![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1") + +Reference-style: +![alt text][logo] + +[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2" + +![Minion](https://octodex.github.com/images/minion.png) +![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") + +Like links, Images also have a footnote style syntax + +![Alt text][id] + +With a reference later in the document defining the URL location: + +[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" +``` + +Here's our logo (hover to see the title text): + +Inline-style: +![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1") + +Reference-style: +![alt text][logo] + +[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2" + +![Minion](https://octodex.github.com/images/minion.png) +![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") + +Like links, Images also have a footnote style syntax + +![Alt text][id] + +With a reference later in the document defining the URL location: + +[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" + +------ + +# [Footnotes](https://github.com/markdown-it/markdown-it-footnote) + +``` +Footnote 1 link[^first]. + +Footnote 2 link[^second]. + +Inline footnote^[Text of inline footnote] definition. + +Duplicated footnote reference[^second]. + +[^first]: Footnote **can have markup** + + and multiple paragraphs. + +[^second]: Footnote text. +``` + +Footnote 1 link[^first]. + +Footnote 2 link[^second]. + +Inline footnote^[Text of inline footnote] definition. + +Duplicated footnote reference[^second]. + +[^first]: Footnote **can have markup** + + and multiple paragraphs. + +[^second]: Footnote text. + +------ + +# Code and Syntax Highlighting + +``` +Inline `code` has `back-ticks around` it. +``` + +Inline `code` has `back-ticks around` it. + +```c# +using System.IO.Compression; + +#pragma warning disable 414, 3021 + +namespace MyApplication +{ + [Obsolete("...")] + class Program : IInterface + { + public static List JustDoIt(int count) + { + Console.WriteLine($"Hello {Name}!"); + return new List(new int[] { 1, 2, 3 }) + } + } +} +``` + +```css +@font-face { + font-family: Chunkfive; src: url('Chunkfive.otf'); +} + +body, .usertext { + color: #F0F0F0; background: #600; + font-family: Chunkfive, sans; +} + +@import url(print.css); +@media print { + a[href^=http]::after { + content: attr(href) + } +} +``` + +```javascript +function $initHighlight(block, cls) { + try { + if (cls.search(/\bno\-highlight\b/) != -1) + return process(block, true, 0x0F) + + ` class="${cls}"`; + } catch (e) { + /* handle exception */ + } + for (var i = 0 / 2; i < classes.length; i++) { + if (checkCondition(classes[i]) === undefined) + console.log('undefined'); + } +} + +export $initHighlight; +``` + +```php +require_once 'Zend/Uri/Http.php'; + +namespace Location\Web; + +interface Factory +{ + static function _factory(); +} + +abstract class URI extends BaseURI implements Factory +{ + abstract function test(); + + public static $st1 = 1; + const ME = "Yo"; + var $list = NULL; + private $var; + + /** + * Returns a URI + * + * @return URI + */ + static public function _factory($stats = array(), $uri = 'http') + { + echo __METHOD__; + $uri = explode(':', $uri, 0b10); + $schemeSpecific = isset($uri[1]) ? $uri[1] : ''; + $desc = 'Multi +line description'; + + // Security check + if (!ctype_alnum($scheme)) { + throw new Zend_Uri_Exception('Illegal scheme'); + } + + $this->var = 0 - self::$st; + $this->list = list(Array("1"=> 2, 2=>self::ME, 3 => \Location\Web\URI::class)); + + return [ + 'uri' => $uri, + 'value' => null, + ]; + } +} + +echo URI::ME . URI::$st1; + +__halt_compiler () ; datahere +datahere +datahere */ +datahere +``` + +------ + +# Tables + +``` +Colons can be used to align columns. + +| Tables | Are | Cool | +| ------------- |:-------------:| -----:| +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +There must be at least 3 dashes separating each header cell. +The outer pipes (|) are optional, and you don't need to make the +raw Markdown line up prettily. You can also use inline Markdown. + +Markdown | Less | Pretty +--- | --- | --- +*Still* | `renders` | **nicely** +1 | 2 | 3 + +| First Header | Second Header | +| ------------- | ------------- | +| Content Cell | Content Cell | +| Content Cell | Content Cell | + +| Command | Description | +| --- | --- | +| git status | List all new or modified files | +| git diff | Show file differences that haven't been staged | + +| Command | Description | +| --- | --- | +| `git status` | List all *new or modified* files | +| `git diff` | Show file differences that **haven't been** staged | + +| Left-aligned | Center-aligned | Right-aligned | +| :--- | :---: | ---: | +| git status | git status | git status | +| git diff | git diff | git diff | + +| Name | Character | +| --- | --- | +| Backtick | ` | +| Pipe | \| | +``` + +Colons can be used to align columns. + +| Tables | Are | Cool | +| ------------- |:-------------:| -----:| +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +There must be at least 3 dashes separating each header cell. +The outer pipes (|) are optional, and you don't need to make the +raw Markdown line up prettily. You can also use inline Markdown. + +Markdown | Less | Pretty +--- | --- | --- +*Still* | `renders` | **nicely** +1 | 2 | 3 + +| First Header | Second Header | +| ------------- | ------------- | +| Content Cell | Content Cell | +| Content Cell | Content Cell | + +| Command | Description | +| --- | --- | +| git status | List all new or modified files | +| git diff | Show file differences that haven't been staged | + +| Command | Description | +| --- | --- | +| `git status` | List all *new or modified* files | +| `git diff` | Show file differences that **haven't been** staged | + +| Left-aligned | Center-aligned | Right-aligned | +| :--- | :---: | ---: | +| git status | git status | git status | +| git diff | git diff | git diff | + +| Name | Character | +| --- | --- | +| Backtick | ` | +| Pipe | \| | + +------ + +# Blockquotes + +``` +> Blockquotes are very handy in email to emulate reply text. +> This line is part of the same quote. + +Quote break. + +> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs right next to each other... +> > > ...or with spaces between arrows. +``` + +> Blockquotes are very handy in email to emulate reply text. +> This line is part of the same quote. + +Quote break. + +> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs right next to each other... +> > > ...or with spaces between arrows. + +------ + +# Inline HTML + +``` +
+
Definition list
+
Is something people use sometimes.
+ +
Markdown in HTML
+
Does *not* work **very** well. Use HTML tags.
+
+``` + +
+
Definition list
+
Is something people use sometimes.
+ +
Markdown in HTML
+
Does *not* work **very** well. Use HTML tags.
+
+ +------ + +# Horizontal Rules + +``` +Three or more... + +--- + +Hyphens + +*** + +Asterisks + +___ + +Underscores +``` + +Three or more... + +--- + +Hyphens + +*** + +Asterisks + +___ + +Underscores + +------ + +# YouTube Videos + +``` + +IMAGE ALT TEXT HERE + +``` + + +IMAGE ALT TEXT HERE + + +``` +[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/YOUTUBE_VIDEO_ID_HERE/0.jpg)](http://www.youtube.com/watch?v=YOUTUBE_VIDEO_ID_HERE) +``` + +[![IMAGE ALT TEXT HERE](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/YouTube_logo_2015.svg/1200px-YouTube_logo_2015.svg.png)](https://www.youtube.com/watch?v=ciawICBvQoE) diff --git a/tests/write_and_read.rs b/tests/write_and_read.rs new file mode 100644 index 0000000..5c8f0f5 --- /dev/null +++ b/tests/write_and_read.rs @@ -0,0 +1,116 @@ +use evp::{exporters::Exporter, prelude::*}; +use outdir_tempdir::TempDir; +use tracing_subscriber::EnvFilter; + +#[test] +fn test_write_and_read() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let dir = TempDir::new(); + let path = dir.path().join("test.evp"); + + let secret_path = format!("{}/tests/ecdsa_private_key.p8", env!("CARGO_MANIFEST_DIR")); + let algo = attesting::Algorithm::ES256; + tracing::info!("Loading private key from: {secret_path}"); + let secret = attesting::Secret::ecdsa_keypair_from_file(algo, &secret_path) + .expect("Failed to read key from file"); + + { + // Create package + let mut package = EvidencePackage::new( + path.clone(), + "EVP Test Package", + &[("EVP Test Tool", "evp-test@example.com")], + ) + .expect("Failed to create package"); + let image_media = package + .add_media(MediaFile::from(include_bytes!("image.jpg"))) + .expect("Failed to read image as media file") + .hash(); + let audio_media = package + .add_media(MediaFile::from(include_bytes!("pour.mp3"))) + .expect("Failed to read audio as media file") + .hash(); + let tc_id = { + let tc = package + .create_test_case("Test Case") + .expect("Failed to create test case"); + tc.evidence_mut().push(Evidence::new( + "text/plain", + EvidenceData::Text { + content: "Hello, world!".to_string(), + }, + )); + tc.evidence_mut().push(Evidence::new( + "text/markdown", + EvidenceData::Text { + content: include_str!("test.md").to_string(), + }, + )); + tc.evidence_mut().push(Evidence::new( + "image/jpeg", + EvidenceData::Media { hash: image_media }, + )); + tc.evidence_mut().push(Evidence::new( + "audio/mp3", + EvidenceData::Media { hash: audio_media }, + )); + *tc.id() + }; + package + .attest_test_case( + tc_id, + &secret, + attesting::RegisteredHeader { + algorithm: algo, + ..Default::default() + }, + ) + .expect("Failed to attest"); + package.save().expect("Failed to save package"); + } + + { + // Read package + let mut package = EvidencePackage::open(path).expect("Failed to read package"); + + // Export package to formats + #[cfg(feature = "exporter-excel")] + let _ = evp::exporters::excel::ExcelExporter + .export_package(&mut package, dir.path().join("export.xlsx")); + #[cfg(feature = "exporter-html")] + let _ = evp::exporters::html::HtmlExporter + .export_package(&mut package, dir.path().join("export.html")); + + let cases = package + .test_case_iter() + .expect("Failed to load test cases") + .cloned() + .collect::>(); + let mut count = 0; + for tc in cases { + count += 1; + let attestations = package + .test_case_attestations(*tc.id()) + .expect("Failed to parse attestations"); + assert_eq!(attestations.len(), 1); + tracing::debug!("{attestations:?}"); + let (ats, same) = &attestations[0]; + assert_eq!(*same, true); + let _ = ats + .decode(&secret, algo) + .expect("Failed to validate attestation"); + assert_eq!(tc.metadata().title(), "Test Case"); + assert_eq!(tc.evidence().len(), 4); + let ev = tc.evidence()[0].clone(); + assert_eq!(ev.kind(), "text/plain"); + assert_eq!( + String::from_utf8_lossy(&ev.data(&mut package)), + "Hello, world!" + ); + } + assert_eq!(count, 1); + } +}