From d6e918c519533f043958df9245f2154f4612f67e Mon Sep 17 00:00:00 2001 From: Lily Hopkins Date: Thu, 21 Aug 2025 00:09:18 +0100 Subject: [PATCH 1/4] refactor: migrate evp out --- .github/workflows/publish-docs.yaml | 4 +- .github/workflows/publish-schemas.yaml | 38 - .vscode/settings.json | 3 +- Cargo.lock | 133 ++-- Cargo.toml | 108 +-- angelmark/.gitignore | 2 - angelmark/Cargo.toml | 13 - angelmark/README.md | 8 - angelmark/clippy.toml | 1 - angelmark/src/angelmark.pest | 100 --- angelmark/src/error.rs | 9 - angelmark/src/lexer.rs | 5 - angelmark/src/lib.rs | 535 -------------- angelmark/src/line.rs | 145 ---- angelmark/src/table.rs | 276 -------- angelmark/src/text.rs | 63 -- angelmark/src/traits.rs | 6 - angelmark/tests/angelmark.md | 31 - build.rs | 44 -- cli/Cargo.toml | 23 + .../src}/angelmark.rs | 2 +- .../src}/arg_parser.rs | 0 {src/evidenceangel-cli => cli/src}/export.rs | 2 +- {src/evidenceangel-cli => cli/src}/main.rs | 0 {src/evidenceangel-cli => cli/src}/package.rs | 2 +- .../src}/result/data.rs | 0 .../src}/result/error.rs | 6 +- .../src}/result/mod.rs | 0 .../src}/result/traits.rs | 0 .../src}/test_cases.rs | 4 +- schemas/.gitignore | 4 - schemas/build.sh | 7 - schemas/draft-hopkins-evp-spec.md | 435 ------------ schemas/manifest.1.schema.json | 76 -- schemas/manifest.2.schema.json | 101 --- schemas/testcase.1.schema.json | 82 --- src/exporters.rs | 41 -- src/exporters/excel.rs | 536 -------------- src/exporters/html.css | 175 ----- src/exporters/html.js | 42 -- src/exporters/html.rs | 480 ------------- src/exporters/zip_of_files.rs | 199 ------ src/lib.rs | 28 - src/lock_file.rs | 85 --- src/package.rs | 661 ------------------ src/package/manifest.rs | 208 ------ src/package/media.rs | 36 - src/package/test_cases.rs | 313 --------- src/result.rs | 67 -- src/zip_read_writer.rs | 210 ------ ui/Cargo.toml | 40 ++ ui/build.rs | 39 ++ {docs => ui/docs}/.gitignore | 0 {docs => ui/docs}/book.toml | 0 {docs => ui/docs}/src/SUMMARY.md | 0 {docs => ui/docs}/src/cli.md | 0 {docs => ui/docs}/src/creating_a_package.md | 0 {docs => ui/docs}/src/creating_a_test_case.md | 0 {docs => ui/docs}/src/exporting.md | 0 {docs => ui/docs}/src/getting_started.md | 0 {docs => ui/docs}/src/glossary.md | 0 .../creating_a_package/0_nothing_is_open.png | Bin .../creating_a_package/1_menu_button.png | Bin .../images/creating_a_package/2_menu_new.png | Bin .../creating_a_package/3_no_case_open.png | Bin .../images/creating_a_package/4_metadata.png | Bin .../creating_a_package/5_package_metadata.png | Bin .../creating_a_package/6_custom_metadata.png | Bin .../0_create_test_case.png | Bin .../creating_a_test_case/2_new_test_case.png | Bin .../creating_a_test_case/3_test_case_edit.png | Bin .../images/creating_a_test_case/4_unsaved.png | Bin .../5_test_case_actions.png | Bin .../src/images/exporting/0_menu_button.png | Bin .../src/images/exporting/1_menu_export.png | Bin .../src/images/exporting/2_select_format.png | Bin .../images/exporting/3_select_destination.png | Bin .../docs}/src/images/rebuild_images.sh | 0 .../docs}/src/images/sources/export.png | Bin {docs => ui/docs}/src/images/sources/menu.png | Bin .../docs}/src/images/sources/new_package.png | Bin .../docs}/src/images/sources/nothing_open.png | Bin .../images/sources/overlays/add_evidence.png | Bin .../images/sources/overlays/add_evidence.svg | 0 .../sources/overlays/create_case_button.png | Bin .../sources/overlays/create_case_button.svg | 0 .../images/sources/overlays/export_format.png | Bin .../images/sources/overlays/export_format.svg | 0 .../images/sources/overlays/export_target.png | Bin .../images/sources/overlays/export_target.svg | 0 .../images/sources/overlays/menu_button.png | Bin .../images/sources/overlays/menu_button.svg | 0 .../images/sources/overlays/menu_export.png | Bin .../images/sources/overlays/menu_export.svg | 0 .../src/images/sources/overlays/menu_new.png | Bin .../src/images/sources/overlays/menu_new.svg | 0 .../sources/overlays/menu_paste_evidence.png | Bin .../sources/overlays/menu_paste_evidence.svg | 0 .../images/sources/overlays/nav_metadata.png | Bin .../images/sources/overlays/nav_metadata.svg | 0 .../sources/overlays/package_metadata.png | Bin .../sources/overlays/package_metadata.svg | 0 .../package_metadata_custom_fields.png | Bin .../package_metadata_custom_fields.svg | 0 .../sources/overlays/test_case_actions.png | Bin .../sources/overlays/test_case_actions.svg | 0 .../sources/overlays/test_case_metadata.png | Bin .../sources/overlays/test_case_metadata.svg | 0 .../src/images/sources/package_metadata.png | Bin .../docs}/src/images/sources/test_case.png | Bin .../docs}/src/images/sources/unsaved.png | Bin .../images/taking_evidence/0_menu_button.png | Bin .../taking_evidence/1_menu_paste_evidence.png | Bin .../images/taking_evidence/2_add_evidence.png | Bin {docs => ui/docs}/src/taking_evidence.md | 0 file_icon.png => ui/file_icon.png | Bin .../hicolor-icon.gresource.xml | 0 icon.png => ui/icon.png | Bin {resources => ui/resources}/evidenceangel.svg | 0 {src/evidenceangel-ui => ui/src}/about.rs | 0 {src/evidenceangel-ui => ui/src}/app.rs | 14 +- .../src}/author_factory.rs | 2 +- .../src}/custom_metadata_editor_factory.rs | 2 +- .../src}/custom_metadata_factory.rs | 2 +- .../src}/dialogs/add_evidence.rs | 2 +- .../src}/dialogs/custom_metadata_field.rs | 0 .../src}/dialogs/error.rs | 9 +- .../src}/dialogs/export.rs | 0 .../src}/dialogs/mod.rs | 0 .../src}/dialogs/new_author.rs | 2 +- .../src}/evidence_factory/file.rs | 2 +- .../src}/evidence_factory/http.rs | 0 .../src}/evidence_factory/image.rs | 2 +- .../src}/evidence_factory/mod.rs | 2 +- .../src}/evidence_factory/rich_text.rs | 2 +- .../src}/evidence_factory/text.rs | 0 {src/evidenceangel-ui => ui/src}/filter.rs | 0 {src/evidenceangel-ui => ui/src}/lang.rs | 2 +- .../src}/locales/en/main.ftl | 0 .../src}/locales/sv/main.ftl | 0 {src/evidenceangel-ui => ui/src}/main.rs | 0 .../src}/nav_factory.rs | 2 +- {src/evidenceangel-ui => ui/src}/util.rs | 2 +- {wix => ui/wix}/.gitignore | 0 {wix => ui/wix}/banner.bmp | Bin {wix => ui/wix}/generate_wix_script.sh | 0 {wix => ui/wix}/license.rtf | 0 {wix => ui/wix}/main.wxs.in | 0 148 files changed, 226 insertions(+), 5249 deletions(-) delete mode 100644 .github/workflows/publish-schemas.yaml delete mode 100644 angelmark/.gitignore delete mode 100644 angelmark/Cargo.toml delete mode 100644 angelmark/README.md delete mode 100644 angelmark/clippy.toml delete mode 100644 angelmark/src/angelmark.pest delete mode 100644 angelmark/src/error.rs delete mode 100644 angelmark/src/lexer.rs delete mode 100644 angelmark/src/lib.rs delete mode 100644 angelmark/src/line.rs delete mode 100644 angelmark/src/table.rs delete mode 100644 angelmark/src/text.rs delete mode 100644 angelmark/src/traits.rs delete mode 100644 angelmark/tests/angelmark.md delete mode 100644 build.rs create mode 100644 cli/Cargo.toml rename {src/evidenceangel-cli => cli/src}/angelmark.rs (96%) rename {src/evidenceangel-cli => cli/src}/arg_parser.rs (100%) rename {src/evidenceangel-cli => cli/src}/export.rs (99%) rename {src/evidenceangel-cli => cli/src}/main.rs (100%) rename {src/evidenceangel-cli => cli/src}/package.rs (99%) rename {src/evidenceangel-cli => cli/src}/result/data.rs (100%) rename {src/evidenceangel-cli => cli/src}/result/error.rs (96%) rename {src/evidenceangel-cli => cli/src}/result/mod.rs (100%) rename {src/evidenceangel-cli => cli/src}/result/traits.rs (100%) rename {src/evidenceangel-cli => cli/src}/test_cases.rs (99%) delete mode 100644 schemas/.gitignore delete mode 100755 schemas/build.sh delete mode 100644 schemas/draft-hopkins-evp-spec.md delete mode 100644 schemas/manifest.1.schema.json delete mode 100644 schemas/manifest.2.schema.json delete mode 100644 schemas/testcase.1.schema.json delete mode 100644 src/exporters.rs delete mode 100644 src/exporters/excel.rs delete mode 100644 src/exporters/html.css delete mode 100644 src/exporters/html.js delete mode 100644 src/exporters/html.rs delete mode 100644 src/exporters/zip_of_files.rs delete mode 100644 src/lib.rs delete mode 100644 src/lock_file.rs delete mode 100644 src/package.rs delete mode 100644 src/package/manifest.rs delete mode 100644 src/package/media.rs delete mode 100644 src/package/test_cases.rs delete mode 100644 src/result.rs delete mode 100644 src/zip_read_writer.rs create mode 100644 ui/Cargo.toml create mode 100644 ui/build.rs rename {docs => ui/docs}/.gitignore (100%) rename {docs => ui/docs}/book.toml (100%) rename {docs => ui/docs}/src/SUMMARY.md (100%) rename {docs => ui/docs}/src/cli.md (100%) rename {docs => ui/docs}/src/creating_a_package.md (100%) rename {docs => ui/docs}/src/creating_a_test_case.md (100%) rename {docs => ui/docs}/src/exporting.md (100%) rename {docs => ui/docs}/src/getting_started.md (100%) rename {docs => ui/docs}/src/glossary.md (100%) rename {docs => ui/docs}/src/images/creating_a_package/0_nothing_is_open.png (100%) rename {docs => ui/docs}/src/images/creating_a_package/1_menu_button.png (100%) rename {docs => ui/docs}/src/images/creating_a_package/2_menu_new.png (100%) rename {docs => ui/docs}/src/images/creating_a_package/3_no_case_open.png (100%) rename {docs => ui/docs}/src/images/creating_a_package/4_metadata.png (100%) rename {docs => ui/docs}/src/images/creating_a_package/5_package_metadata.png (100%) rename {docs => ui/docs}/src/images/creating_a_package/6_custom_metadata.png (100%) rename {docs => ui/docs}/src/images/creating_a_test_case/0_create_test_case.png (100%) rename {docs => ui/docs}/src/images/creating_a_test_case/2_new_test_case.png (100%) rename {docs => ui/docs}/src/images/creating_a_test_case/3_test_case_edit.png (100%) rename {docs => ui/docs}/src/images/creating_a_test_case/4_unsaved.png (100%) rename {docs => ui/docs}/src/images/creating_a_test_case/5_test_case_actions.png (100%) rename {docs => ui/docs}/src/images/exporting/0_menu_button.png (100%) rename {docs => ui/docs}/src/images/exporting/1_menu_export.png (100%) rename {docs => ui/docs}/src/images/exporting/2_select_format.png (100%) rename {docs => ui/docs}/src/images/exporting/3_select_destination.png (100%) rename {docs => ui/docs}/src/images/rebuild_images.sh (100%) rename {docs => ui/docs}/src/images/sources/export.png (100%) rename {docs => ui/docs}/src/images/sources/menu.png (100%) rename {docs => ui/docs}/src/images/sources/new_package.png (100%) rename {docs => ui/docs}/src/images/sources/nothing_open.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/add_evidence.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/add_evidence.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/create_case_button.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/create_case_button.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/export_format.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/export_format.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/export_target.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/export_target.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_button.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_button.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_export.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_export.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_new.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_new.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_paste_evidence.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/menu_paste_evidence.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/nav_metadata.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/nav_metadata.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/package_metadata.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/package_metadata.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/package_metadata_custom_fields.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/package_metadata_custom_fields.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/test_case_actions.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/test_case_actions.svg (100%) rename {docs => ui/docs}/src/images/sources/overlays/test_case_metadata.png (100%) rename {docs => ui/docs}/src/images/sources/overlays/test_case_metadata.svg (100%) rename {docs => ui/docs}/src/images/sources/package_metadata.png (100%) rename {docs => ui/docs}/src/images/sources/test_case.png (100%) rename {docs => ui/docs}/src/images/sources/unsaved.png (100%) rename {docs => ui/docs}/src/images/taking_evidence/0_menu_button.png (100%) rename {docs => ui/docs}/src/images/taking_evidence/1_menu_paste_evidence.png (100%) rename {docs => ui/docs}/src/images/taking_evidence/2_add_evidence.png (100%) rename {docs => ui/docs}/src/taking_evidence.md (100%) rename file_icon.png => ui/file_icon.png (100%) rename hicolor-icon.gresource.xml => ui/hicolor-icon.gresource.xml (100%) rename icon.png => ui/icon.png (100%) rename {resources => ui/resources}/evidenceangel.svg (100%) rename {src/evidenceangel-ui => ui/src}/about.rs (100%) rename {src/evidenceangel-ui => ui/src}/app.rs (99%) rename {src/evidenceangel-ui => ui/src}/author_factory.rs (98%) rename {src/evidenceangel-ui => ui/src}/custom_metadata_editor_factory.rs (99%) rename {src/evidenceangel-ui => ui/src}/custom_metadata_factory.rs (98%) rename {src/evidenceangel-ui => ui/src}/dialogs/add_evidence.rs (99%) rename {src/evidenceangel-ui => ui/src}/dialogs/custom_metadata_field.rs (100%) rename {src/evidenceangel-ui => ui/src}/dialogs/error.rs (87%) rename {src/evidenceangel-ui => ui/src}/dialogs/export.rs (100%) rename {src/evidenceangel-ui => ui/src}/dialogs/mod.rs (100%) rename {src/evidenceangel-ui => ui/src}/dialogs/new_author.rs (99%) rename {src/evidenceangel-ui => ui/src}/evidence_factory/file.rs (98%) rename {src/evidenceangel-ui => ui/src}/evidence_factory/http.rs (100%) rename {src/evidenceangel-ui => ui/src}/evidence_factory/image.rs (98%) rename {src/evidenceangel-ui => ui/src}/evidence_factory/mod.rs (99%) rename {src/evidenceangel-ui => ui/src}/evidence_factory/rich_text.rs (99%) rename {src/evidenceangel-ui => ui/src}/evidence_factory/text.rs (100%) rename {src/evidenceangel-ui => ui/src}/filter.rs (100%) rename {src/evidenceangel-ui => ui/src}/lang.rs (98%) rename {src/evidenceangel-ui => ui/src}/locales/en/main.ftl (100%) rename {src/evidenceangel-ui => ui/src}/locales/sv/main.ftl (100%) rename {src/evidenceangel-ui => ui/src}/main.rs (100%) rename {src/evidenceangel-ui => ui/src}/nav_factory.rs (99%) rename {src/evidenceangel-ui => ui/src}/util.rs (96%) rename {wix => ui/wix}/.gitignore (100%) rename {wix => ui/wix}/banner.bmp (100%) rename {wix => ui/wix}/generate_wix_script.sh (100%) rename {wix => ui/wix}/license.rtf (100%) rename {wix => ui/wix}/main.wxs.in (100%) diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index efbf733..cb5291b 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -24,7 +24,7 @@ jobs: echo "CARGO_PKG_PRERELEASE=${CARGO_PKG_PRERELEASE}" >> $GITHUB_OUTPUT - name: Build book run: | - cd docs || exit + cd ui/docs || exit cargo install mdbook mdbook build -d ${{ steps.version.outputs.CARGO_PKG_VERSION }} - name: Upload documentation @@ -33,7 +33,7 @@ jobs: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} - source: docs/${{ steps.version.outputs.CARGO_PKG_VERSION }} + source: ui/docs/${{ steps.version.outputs.CARGO_PKG_VERSION }} target: ${{ secrets.SSH_DOCS_TARGET_PATH }} diff --git a/.github/workflows/publish-schemas.yaml b/.github/workflows/publish-schemas.yaml deleted file mode 100644 index a64282a..0000000 --- a/.github/workflows/publish-schemas.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: Publish Schemas - -on: - push: - branches: - - main - -jobs: - build: - name: Build schema GH pages - runs-on: ubuntu-latest - permissions: write-all - - steps: - - uses: actions/checkout@v3 - - uses: actions/upload-pages-artifact@v3 - with: - path: schemas/ - - deploy: - needs: build - - # Grant GITHUB_TOKEN the permissions required to make a Pages deployment - permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source - - # Deploy to the github-pages environment - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - # Specify runner + deployment step - runs-on: ubuntu-latest - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ae3c08..dc7c9f3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,6 @@ "rust-analyzer.cargo.features": "all", "conventionalCommits.scopes": [ "cli", - "exporters", - "ui" + "ui", ] } diff --git a/Cargo.lock b/Cargo.lock index 8e8dbe0..4b24f68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,18 +79,6 @@ dependencies = [ "libc", ] -[[package]] -name = "angelmark" -version = "0.0.0" -dependencies = [ - "getset", - "pest", - "pest_derive", - "regex", - "thiserror 2.0.12", - "tracing", -] - [[package]] name = "anstream" version = "0.6.19" @@ -149,24 +137,30 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn 2.0.104", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -610,9 +604,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -781,46 +775,77 @@ dependencies = [ ] [[package]] -name = "evidenceangel" +name = "evidenceangel-cli" version = "1.6.0-alpha.2" dependencies = [ - "angelmark", - "base64", - "build_html", "chrono", "clap", "clap-verbosity-flag", "clap_complete", "colored", + "evp", + "getset", + "parse_datetime", + "schemars", + "serde", + "serde_json", + "thiserror 2.0.16", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "evidenceangel-ui" +version = "1.6.0-alpha.2" +dependencies = [ + "chrono", + "clap", "directories", + "evp", "fluent-templates", "getset", "glib-build-tools", - "html-escape", "ico-builder", "infer", - "jsonschema", "mdbook", "open", "parking_lot", "parse_datetime", "relm4", "relm4-icons", - "rust_xlsxwriter", - "schemars", - "serde", - "serde_json", - "sha256", "sys-locale", "tempfile", - "thiserror 2.0.12", "tracing", "tracing-panic", - "tracing-subscriber", "tracing-subscriber-multi", "uuid", - "winapi", "winresource", +] + +[[package]] +name = "evp" +version = "1.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90451314450995c892c539d881a4582ec73388367dc2e4700e9ff12e72acf6ca" +dependencies = [ + "base64", + "build_html", + "chrono", + "getset", + "html-escape", + "infer", + "jsonschema", + "pest", + "pest_derive", + "regex", + "rust_xlsxwriter", + "serde", + "serde_json", + "sha256", + "thiserror 2.0.16", + "tracing", + "uuid", "zip", ] @@ -1538,7 +1563,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -1630,18 +1655,20 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -2060,9 +2087,9 @@ dependencies = [ [[package]] name = "libbz2-rs-sys" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775bf80d5878ab7c2b1080b5351a48b2f737d9f6f8b383574eebcc22be0dfccb" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" @@ -2072,9 +2099,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "liblzma" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0791ab7e08ccc8e0ce893f6906eb2703ed8739d8e89b57c0714e71bad09024c8" +checksum = "272b875472a046e39ff7408374a5a050b112d2142211a0f54a295c0bd1c3c757" dependencies = [ "liblzma-sys", ] @@ -2519,7 +2546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", "ucd-trie", ] @@ -2840,7 +2867,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -2955,9 +2982,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", @@ -3150,9 +3177,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -3438,11 +3465,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -3458,9 +3485,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -3820,9 +3847,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 78a0e27..a43d661 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,107 +1,17 @@ -[package] -name = "evidenceangel" -description = "Library and executables to work with EvidenceAngel evidence packages (*.evp)." +[workspace] +resolver = "3" +members = [ + "cli", + "ui", +] + +[workspace.package] version = "1.6.0-alpha.2" -edition = "2024" license = "GPL-3.0-or-later" +edition = "2024" authors = [ "Lily Hopkins ", "Eden Turner ", ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[[bin]] -name = "evidenceangel-cli" -path = "src/evidenceangel-cli/main.rs" -required-features = ["cli"] - -[[bin]] -name = "evidenceangel-ui" -path = "src/evidenceangel-ui/main.rs" -required-features = ["ui"] - -[features] -default = ["exporter-html", "exporter-zip-of-files"] -cli = [ - "exporter-html", - "exporter-excel", - "exporter-zip-of-files", - "dep:clap", - "dep:clap_complete", - "dep:clap-verbosity-flag", - "dep:colored", - "dep:parse_datetime", - "dep:schemars", - "dep:tracing-subscriber", -] -exporter-excel = ["dep:rust_xlsxwriter"] -exporter-html = ["dep:build_html", "dep:html-escape"] -exporter-zip-of-files = [] -ui = [ - "dep:clap", - "dep:directories", - "dep:fluent-templates", - "dep:glib-build-tools", - "dep:open", - "dep:parse_datetime", - "dep:parking_lot", - "dep:relm4", - "dep:relm4-icons", - "dep:sys-locale", - "dep:tempfile", - "dep:tracing-subscriber-multi", - "dep:tracing-panic", - "exporter-html", - "exporter-excel", - "exporter-zip-of-files", -] -windows-keep-console-window = [] - -[dependencies] -angelmark = { path = "angelmark" } -base64 = "0.22.1" -build_html = { version = "2.5.0", optional = true } -chrono = { version = "0.4.38", features = ["serde"] } -clap = { version = "4.5.4", features = ["derive"], optional = true } -clap-verbosity-flag = { version = "3.0.2", default-features = false, features = ["tracing"], optional = true } -clap_complete = { version = "4.5.2", optional = true } -colored = { version = "3.0.0", optional = true } -directories = { version = "6.0.0", optional = true } -fluent-templates = { version = "0.13.0", optional = true } -getset = "0.1.2" -html-escape = { version = "0.2.13", optional = true } -infer = "0.19.0" -jsonschema = "0.31.0" -open = { version = "5.3.0", optional = true } -parking_lot = { version = "0.12.3", optional = true } -parse_datetime = { version = "0.10.0", optional = true } -relm4 = { version = "0.9.0", features = [ - "libadwaita", - "gnome_46", -], optional = true } -relm4-icons = { version = "0.9.0", optional = true } -rust_xlsxwriter = { version = "0.89.1", features = ["chrono"], optional = true } -schemars = { version = "1.0.4", features = ["chrono04"], optional = true } -serde = { version = "1.0.200", features = ["derive"] } -serde_json = "1.0.116" -sha256 = "1.5.0" -sys-locale = { version = "0.3.1", optional = true } -tempfile = { version = "3.20.0", optional = true } -thiserror = "2.0.4" -tracing = "0.1.41" -tracing-panic = { version = "0.1.2", optional = true } -tracing-subscriber = { version = "0.3.19", optional = true } -tracing-subscriber-multi = { version = "0.1.0", optional = true } -uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] } -zip = "4.3.0" - -[target.'cfg(windows)'.dependencies] -winapi = "0.3.9" - -[target.'cfg(windows)'.build-dependencies] -winresource = "0.1" -ico-builder = "0.1" - -[build-dependencies] -glib-build-tools = { version = "0.21.0", optional = true } -mdbook = { version = "0.4.48", default-features = false, features = ["search"] } diff --git a/angelmark/.gitignore b/angelmark/.gitignore deleted file mode 100644 index 2c96eb1..0000000 --- a/angelmark/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target/ -Cargo.lock diff --git a/angelmark/Cargo.toml b/angelmark/Cargo.toml deleted file mode 100644 index d1946ad..0000000 --- a/angelmark/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "angelmark" -version = "0.0.0" -edition = "2021" -publish = false - -[dependencies] -getset = "0.1.4" -pest = "2.7.15" -pest_derive = "2.7.15" -regex = "1.11.1" -thiserror = "2.0.11" -tracing = "0.1.41" diff --git a/angelmark/README.md b/angelmark/README.md deleted file mode 100644 index 363ca8e..0000000 --- a/angelmark/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Angelmark - -Angelmark is a Markdown-like markup language for EvidenceAngel. - -## Quirks - -- Headings end with an implicit newline. -- Any number of newlines will always be abstracted to a single newline. diff --git a/angelmark/clippy.toml b/angelmark/clippy.toml deleted file mode 100644 index d1ccee7..0000000 --- a/angelmark/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -doc-valid-idents = ["AngelMark", "EvidenceAngel", ".."] diff --git a/angelmark/src/angelmark.pest b/angelmark/src/angelmark.pest deleted file mode 100644 index 49c595a..0000000 --- a/angelmark/src/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/angelmark/src/error.rs b/angelmark/src/error.rs deleted file mode 100644 index 232502e..0000000 --- a/angelmark/src/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/angelmark/src/lexer.rs b/angelmark/src/lexer.rs deleted file mode 100644 index 0d93bed..0000000 --- a/angelmark/src/lexer.rs +++ /dev/null @@ -1,5 +0,0 @@ -use pest_derive::Parser; - -#[derive(Parser)] -#[grammar = "angelmark.pest"] -pub struct AngelmarkParser; diff --git a/angelmark/src/lib.rs b/angelmark/src/lib.rs deleted file mode 100644 index 8621dfc..0000000 --- a/angelmark/src/lib.rs +++ /dev/null @@ -1,535 +0,0 @@ -#![deny(unsafe_code)] -#![warn(clippy::pedantic)] -#![warn(missing_docs)] -#![doc = include_str!("../README.md")] - -mod error; -pub use error::Error; - -mod lexer; -use lexer::Rule; - -mod line; -pub use line::AngelmarkLine; - -mod table; -pub use table::{ - AngelmarkTable, AngelmarkTableAlignment, AngelmarkTableAlignmentCell, - AngelmarkTableAlignmentRow, AngelmarkTableCell, AngelmarkTableRow, -}; - -mod text; -pub use text::AngelmarkText; - -mod traits; -pub use traits::EqIgnoringSpan; - -use getset::Getters; -use pest::{iterators::Pair, Parser, Span}; -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) -} - -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!(), - } -} - -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/angelmark/src/line.rs b/angelmark/src/line.rs deleted file mode 100644 index 1e7060d..0000000 --- a/angelmark/src/line.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::{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/angelmark/src/table.rs b/angelmark/src/table.rs deleted file mode 100644 index d386f51..0000000 --- a/angelmark/src/table.rs +++ /dev/null @@ -1,276 +0,0 @@ -use std::cmp::Ordering; - -use getset::Getters; -use pest::iterators::Pair; - -use crate::{lexer::Rule, AngelmarkText, EqIgnoringSpan, OwnedSpan}; - -/// 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 - } -} - -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(crate::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/angelmark/src/text.rs b/angelmark/src/text.rs deleted file mode 100644 index 824d5b2..0000000 --- a/angelmark/src/text.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{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/angelmark/src/traits.rs b/angelmark/src/traits.rs deleted file mode 100644 index 63bd2d3..0000000 --- a/angelmark/src/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/angelmark/tests/angelmark.md b/angelmark/tests/angelmark.md deleted file mode 100644 index 0bc7ba5..0000000 --- a/angelmark/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/build.rs b/build.rs deleted file mode 100644 index 09d8f52..0000000 --- a/build.rs +++ /dev/null @@ -1,44 +0,0 @@ -fn main() { - if cfg!(feature = "cli") || cfg!(feature = "ui") { - #[cfg(feature = "ui")] - { - // Build hicolor icons - println!("cargo::rerun-if-changed=resources"); - println!("cargo::rerun-if-changed=hicolor-icon.gresource.xml"); - glib_build_tools::compile_resources( - &["resources"], - "hicolor-icon.gresource.xml", - "hicolor-icon.gresource", - ); - } - - // Build documentation - println!("cargo::rerun-if-changed=docs/book.toml"); - println!("cargo::rerun-if-changed=docs/src"); - let docs_book = - mdbook::MDBook::load("docs").expect("Failed to load documentation for EvidenceAngel"); - docs_book - .build() - .expect("Failed to build documentation for EvidenceAngel"); - - // Build icon - println!("cargo::rerun-if-changed=icon.png"); - #[cfg(windows)] - { - ico_builder::IcoBuilder::default() - .add_source_file("icon.png") - .build_file("icon.ico") - .unwrap(); - - ico_builder::IcoBuilder::default() - .add_source_file("file_icon.png") - .build_file("file_icon.ico") - .unwrap(); - - let mut res = winresource::WindowsResource::new(); - res.set_icon("icon.ico"); - res.set_icon_with_id("file_icon.ico", "2"); - res.compile().unwrap(); - } - } -} diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..157dc76 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "evidenceangel-cli" +version.workspace = true +license.workspace = true +edition.workspace = true +authors.workspace = true + +[dependencies] +chrono = { version = "0.4.38", features = ["serde"] } +clap = { version = "4.5.4", features = ["derive"] } +clap-verbosity-flag = { version = "3.0.2", default-features = false, features = ["tracing"] } +clap_complete = "4.5.2" +colored = "3.0.0" +evp = { version = "1.0.0-beta.1", features = ["exporter-excel", "exporter-html", "exporter-zip-of-files"] } +getset = "0.1.6" +parse_datetime = "0.10.0" +schemars = { version = "1.0.4", features = ["chrono04"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.143" +thiserror = "2.0.16" +tracing = "0.1.41" +tracing-subscriber = "0.3.19" +uuid = "1.18.0" diff --git a/src/evidenceangel-cli/angelmark.rs b/cli/src/angelmark.rs similarity index 96% rename from src/evidenceangel-cli/angelmark.rs rename to cli/src/angelmark.rs index 306013c..77548a9 100644 --- a/src/evidenceangel-cli/angelmark.rs +++ b/cli/src/angelmark.rs @@ -1,4 +1,4 @@ -use angelmark::AngelmarkText; +use evp::angelmark::AngelmarkText; use colored::Colorize; /// Convert [`AngelmarkText`] to a string with ANSI symbols for terminal display. diff --git a/src/evidenceangel-cli/arg_parser.rs b/cli/src/arg_parser.rs similarity index 100% rename from src/evidenceangel-cli/arg_parser.rs rename to cli/src/arg_parser.rs diff --git a/src/evidenceangel-cli/export.rs b/cli/src/export.rs similarity index 99% rename from src/evidenceangel-cli/export.rs rename to cli/src/export.rs index c824b9b..6b372e3 100644 --- a/src/evidenceangel-cli/export.rs +++ b/cli/src/export.rs @@ -1,7 +1,7 @@ use std::{fmt, path::PathBuf, rc::Rc}; use clap::Subcommand; -use evidenceangel::{ +use evp::{ EvidencePackage, exporters::{ Exporter, excel::ExcelExporter, html::HtmlExporter, zip_of_files::ZipOfFilesExporter, diff --git a/src/evidenceangel-cli/main.rs b/cli/src/main.rs similarity index 100% rename from src/evidenceangel-cli/main.rs rename to cli/src/main.rs diff --git a/src/evidenceangel-cli/package.rs b/cli/src/package.rs similarity index 99% rename from src/evidenceangel-cli/package.rs rename to cli/src/package.rs index 29e0091..b71d091 100644 --- a/src/evidenceangel-cli/package.rs +++ b/cli/src/package.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, fmt, path::PathBuf, rc::Rc}; use chrono::FixedOffset; use clap::Subcommand; use colored::Colorize; -use evidenceangel::{Author, EvidencePackage}; +use evp::{Author, EvidencePackage}; use schemars::JsonSchema; use serde::Serialize; diff --git a/src/evidenceangel-cli/result/data.rs b/cli/src/result/data.rs similarity index 100% rename from src/evidenceangel-cli/result/data.rs rename to cli/src/result/data.rs diff --git a/src/evidenceangel-cli/result/error.rs b/cli/src/result/error.rs similarity index 96% rename from src/evidenceangel-cli/result/error.rs rename to cli/src/result/error.rs index dac1df8..59b886b 100644 --- a/src/evidenceangel-cli/result/error.rs +++ b/cli/src/result/error.rs @@ -20,11 +20,11 @@ pub enum CliError { /// failed to save the evidence package #[error("failed to save package: {0}")] - FailedToSavePackage(Rc), + FailedToSavePackage(Rc), /// failed to read the evidence package #[error("failed to read package: {0}")] - FailedToReadPackage(Rc), + FailedToReadPackage(Rc), /// invalid export format specified #[error("invalid export format `{0}`")] @@ -32,7 +32,7 @@ pub enum CliError { /// failed to export to file #[error("failed to export: {0}")] - FailedToExport(Rc), + FailedToExport(Rc), /// the provided string is not a one-based index and does not match a single test case exclusively #[error( diff --git a/src/evidenceangel-cli/result/mod.rs b/cli/src/result/mod.rs similarity index 100% rename from src/evidenceangel-cli/result/mod.rs rename to cli/src/result/mod.rs diff --git a/src/evidenceangel-cli/result/traits.rs b/cli/src/result/traits.rs similarity index 100% rename from src/evidenceangel-cli/result/traits.rs rename to cli/src/result/traits.rs diff --git a/src/evidenceangel-cli/test_cases.rs b/cli/src/test_cases.rs similarity index 99% rename from src/evidenceangel-cli/test_cases.rs rename to cli/src/test_cases.rs index 1537460..cacfa15 100644 --- a/src/evidenceangel-cli/test_cases.rs +++ b/cli/src/test_cases.rs @@ -6,11 +6,11 @@ use std::{ rc::Rc, }; -use angelmark::{AngelmarkLine, AngelmarkTableAlignment, parse_angelmark}; +use evp::angelmark::{AngelmarkLine, AngelmarkTableAlignment, parse_angelmark}; use chrono::FixedOffset; use clap::{Subcommand, ValueEnum}; use colored::Colorize; -use evidenceangel::{ +use evp::{ Evidence, EvidenceData, EvidenceKind, EvidencePackage, MediaFile, TestCasePassStatus, }; use schemars::JsonSchema; diff --git a/schemas/.gitignore b/schemas/.gitignore deleted file mode 100644 index b572698..0000000 --- a/schemas/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.xml -*.txt -*.html -*.pdf diff --git a/schemas/build.sh b/schemas/build.sh deleted file mode 100755 index 6399ce0..0000000 --- a/schemas/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -FILE="draft-hopkins-evp-spec.md" -RFCXML=$(basename "$FILE" .md).xml - -mmark $FILE >"$RFCXML" -xml2rfc --v3 --text --html --pdf $RFCXML diff --git a/schemas/draft-hopkins-evp-spec.md b/schemas/draft-hopkins-evp-spec.md deleted file mode 100644 index a3c1f0b..0000000 --- a/schemas/draft-hopkins-evp-spec.md +++ /dev/null @@ -1,435 +0,0 @@ -%%% -title = "Evidence Package Format Specification" -abbrev = "evp-spec" -ipr = "trust200902" -area = "" -workgroup = "" -keyword = ["evp", "evidence", "format", "specification"] -submissionType = "independent" - -[seriesInfo] -name = "Internet-Draft" -value = "draft-hopkins-evp-spec-04" -stream = "independent" -status = "informational" - -date = 2025-07-27T00:00:00Z - -[[author]] -initials="L." -surname="Hopkins" -fullname="Lily Hopkins" - [author.address] - uri = "https://github.com/lilopkins" - email = "lily@hpkns.uk" - -[[author]] -inials="E." -surname="Turner" -fullname="Eden Turner" - [author.address] - uri = "https://github.com/Some-Birb7190" - email = "somebirb7190@gmail.com" -%%% - -.# 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. - -{mainmatter} - -# Introduction - -## Purpose - -The purpose of this specification is to define a format for storage of -test evidence 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; -* 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. - -## Intended Audience - -This specification is intended for those who might wish to write their -own implementation of the evidence package format. There are a number of -situations where writing an implementation may be desirable: - -* in an automation tool that runs a number of operations to - automatically test something, to produce an evidence package - containing the results of the automated testing; -* in a manual evidence collection tool, where a user might want to - collect evidence in a single, easy to manage place for later - processing or sharing; -* in an analysis tool, to view, annotate, share and understand the - evidence from previous testing; -* in a viewer, to view evidence that has been shared, for example from a - testing team to a customer, or; -* any other situation where it may be desirable to collect test evidence - and bundle it together for later. - -## Changes from Previous Versions - -This document forms the original specification. - -# Terminology - -The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", -"**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", -"**NOT RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document -are to be interpreted as described in BCP 14 [@!RFC2119] [@!RFC8174] -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. - - - - .ZIP File Format Specification - - PKWARE, Inc. - - - - - -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: - -| Element | Condition | Type | Section | Description | -|------------|-----------|------|---|---| -| $schema | Optional | String | | 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. | -| 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). - -### "metadata" Element {#manifest-metadata} - -| Element | Condition | Type | Section | Description | -|---------|-----------|------|---|---| -| title | Mandatory | String | | The name of the evidence package. | -| authors | Mandatory | Array | (#manifest-metadata-authors) | The authors attributed to this evidence package. | - -#### "authors" Array Element {#manifest-metadata-authors} - -| Element | Condition | Type | Description | -|---------|-----------|------|---| -| 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} - -Elements within this object will become custom metadata properties for -test cases in this package. Each object **MUST** have the following -fields: - -| Element | Condition | Type | Section | Description | -|-------------|-----------|------|---|---| -| name | Mandatory | String | | The name of this custom metadata field. | -| description | Mandatory | String | (#manifest-metadata-authors) | The description of this custom metadata field. | -| primary | Mandatory | Boolean | (#manifest-custom-metadata-primary) | Is this custom field primary? | - -#### "primary" Boolean {#manifest-custom-metadata-primary} - -The "primary" value of custom metadata fields **MAY** be false for all -fields, or **MAY** be true for exactly one field. It **MUST NOT** be -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 -to show the primary custom metadata value for each test case alongside -it. - -### "media" Array Element {#manifest-media} - -| Element | Condition | Type | Description | -|-----------------|-----------|------|---| -| sha256_checksum | Mandatory | String | The SHA256 checksum of the associated media file. | -| mime_type | Mandatory | String | The Internet Media Type [@!RFC2046] of the associated media file. | - -### "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". | - -## "testcases" 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]. - -### ".json" File - -| Element | Condition | Type | Section | Description | -|----------|-----------|------|---|---| -| $schema | Optional | String | | 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). - -#### "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. | -| 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. -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. - -#### "evidence" Array Element {#test-case-evidence} - -| Element | Condition | Type | Section | Description | -|-------------------|-----------|------|---|---| -| kind | Mandatory | String | (#evidence-kind) | The type 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. | - -##### "kind" {#evidence-kind} - -The "kind" of evidence **MUST** be one of "Text", "RichText", "Image", -"Http", "File". - -For more information about each type, see (#kinds-of-evidence). - -##### "value" {#evidence-value} - -The "value" **MUST** be one of the following acceptable patterns: - -* "plain:" followed by plain text; -* "media:" followed by a media file SHA256 hash, or; -* "base64:" followed by a base64 string of data without padding. - -## "media" Directory - -The "media" directory stores data in files within the ZIP archive that -would be otherwise impractical to store directly in the test cases. - -Files stored in this directory are of abitrary type. They **MUST** be -named by their SHA256 checksum [@!RFC6234] with no extension. Their -SHA256 checksum and media type **MUST** be stored in the package -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 -**MAY** choose to store the clashing file as base64 data instead of as -an additional media file. - -# Handling an Evidence Package - -## Locking - -When loading an evidence package, implemetors **MUST** use a lock file -with the file name ".~lock." followed by the full name of the package it -protects, followed by "#", for example for a package called -"example.evp", the lock file **MUST** be called ".~lock.example.evp#". -It **MUST** be located adjacent (in the same directory as) the evidence -package. The file **MUST** contain the process ID of the process holding -the lock. - -The lock file should be considered as locking the package if it is -present, regardless of contents. - -If either of these is not the case, it should be assumed that the there -is no current lock over the package. - -## Media Loading - -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 -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: - -| 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. | - -Implementors **MUST** support all of these kinds, and **MUST NOT** -introduce new kinds. - -## RichText's Markdown - -The RichText evidence kind supports a very limited version of markdown: - -* Headings 1-6 -* Bold, Italic, Monospace -* Tables -* Code blocks with syntax highlighting - -Implementors **MUST NOT** process any other markup. - -## HTTP Requests - -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>>: - -~~~http -GET / HTTP/1.1 -Host: example.com -User-Agent: HTTPie - -\x1eHTTP/1.1 200 OK //<<1>> -Cache-Control: max-age=1366 -Connection: close -... -~~~ - -# Extending Behaviours of an Evidence Package - -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 -additional fields. - -When an implementor 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. - -# Security Considerations - -The evidence package format can store arbitrary files that may or may -not be executable. Implementors **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. - -{backmatter} - -# Example Archive Layout {#example-archive} - -~~~ -example.evp - |- manifest.json - |- media - | \- 203073da0b36a5921f2914e2093abcae7eb987846f405b438c25792bab1617fa - \- testcases - \- eabb5d31-a958-4609-ac98-83365e14d18b.json -~~~ - -# Example Package Manifest JSON {#example-manifest} - -~~~json -{ - "metadata": { - "title": "Example Evidence Package", - "authors": [ - { - "name": "Anonymous Author" - }, - { - "name": "Lily Hopkins", - "email": "lily@hpkns.uk" - } - ] - }, - "custom_test_case_metadata": { - "example": { - "name": "Example Metadata Field", - "description": "A field showing that custom fields can be added", - "primary": true - } - }, - "media": [ - { - "sha256_checksum": "203073da0b36a5921f2914e2093abcae7eb987846f405b438c25792bab1617fa", - "mime_type": "text/plain" - } - ], - "test_cases": [ - { - "id": "eabb5d31-a958-4609-ac98-83365e14d18b" - } - ] -} -~~~ - -# Example Test Case Manifest JSON {#example-test-case} - -~~~json -{ - "metadata": { - "title": "Example Test Case", - "execution_datetime": "2025-05-01T11:13:29+01:00", - "passed": null, - "custom": { - "example": "Example custom metadata field value" - } - }, - "evidence": [ - { - "kind":"Text", - "value":"plain:This is some text based evidence" - }, - { - "kind":"Text", - "value":"base64:VGhpcyBpcyBzb21lIHRleHQgYmFzZWQgYmFzZTY0IGVuY29kZWQgZXZpZGVuY2U" - }, - { - "kind":"File", - "value":"media:203073da0b36a5921f2914e2093abcae7eb987846f405b438c25792bab1617fa", - "caption": "An example file", - "original_filename": "example.txt" - } - ] -} -~~~ - -# JSON Schema for Package Manifest - -<{{manifest.2.schema.json}} - -# JSON Schema for Test Case Manifest - -<{{testcase.1.schema.json}} diff --git a/schemas/manifest.1.schema.json b/schemas/manifest.1.schema.json deleted file mode 100644 index edc4ce7..0000000 --- a/schemas/manifest.1.schema.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://evidenceangel-schemas.hpkns.uk/manifest.1.schema.json", - "$schema": "http://json-schema.org/draft-07/schema", - "type": "object", - "description": "The metadata file `metadata.json` as part of an evidence package.", - "properties": { - "metadata": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The name of the evidence package.", - "minLength": 1, - "maxLength": 30 - }, - "authors": { - "type": "array", - "description": "The authors attributed to this evidence package.", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The author's name." - }, - "email": { - "type": "string", - "description": "The author's email address, although format is not verified." - } - }, - "required": ["name"] - } - }, - "description": { - "type": "string", - "description": "An optional description of the package." - } - }, - "required": ["title", "authors"] - }, - "media": { - "type": "array", - "items": { - "type": "object", - "description": "A media entry. When an entry is present in this manifest, it MUST also be present in the `media` directory of the package.", - "properties": { - "sha256_checksum": { - "type": "string", - "description": "The SHA256 checksum of the media file. This MUST also match identically the name of the file with no extension in the `media` directory.", - "pattern": "^[0-9a-f]{64}$" - }, - "mime_type": { - "type": "string", - "description": "The MIME type of the media file." - } - }, - "required": ["sha256_checksum", "mime_type"] - } - }, - "test_cases": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "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`." - } - }, - "required": ["name"] - } - } - }, - "required": ["metadata", "media", "test_cases"] -} diff --git a/schemas/manifest.2.schema.json b/schemas/manifest.2.schema.json deleted file mode 100644 index 539b35a..0000000 --- a/schemas/manifest.2.schema.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "$id": "https://evidenceangel-schemas.hpkns.uk/manifest.2.schema.json", - "$schema": "http://json-schema.org/draft-07/schema", - "type": "object", - "description": "The metadata file `metadata.json` as part of an evidence package.", - "properties": { - "metadata": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The name of the evidence package.", - "minLength": 1, - "maxLength": 30 - }, - "authors": { - "type": "array", - "description": "The authors attributed to this evidence package.", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The author's name." - }, - "email": { - "type": "string", - "description": "The author's email address, although format is not verified." - } - }, - "required": ["name"] - } - }, - "description": { - "type": "string", - "description": "An optional description of the package." - } - }, - "required": ["title", "authors"] - }, - "custom_test_case_metadata": { - "type": "object", - "description": "Custom metadata fields for test cases", - "patternProperties": { - ".+": { - "type": "object", - "description": "A custom metadata field", - "properties": { - "name": { - "type": "string", - "description": "A user-friendly name for this custom property." - }, - "description": { - "type": "string", - "description": "A description for this custom property." - }, - "primary": { - "type": "boolean", - "description": "Is this custom property the main one in this package? This may influence how it is displayed in editors." - } - }, - "required": ["name", "description", "primary"] - } - } - }, - "media": { - "type": "array", - "items": { - "type": "object", - "description": "A media entry. When an entry is present in this manifest, it MUST also be present in the `media` directory of the package.", - "properties": { - "sha256_checksum": { - "type": "string", - "description": "The SHA256 checksum of the media file. This MUST also match identically the name of the file with no extension in the `media` directory.", - "pattern": "^[0-9a-f]{64}$" - }, - "mime_type": { - "type": "string", - "description": "The MIME type of the media file." - } - }, - "required": ["sha256_checksum", "mime_type"] - } - }, - "test_cases": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "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`." - } - }, - "required": ["id"] - } - } - }, - "required": ["metadata", "media", "test_cases"] -} diff --git a/schemas/testcase.1.schema.json b/schemas/testcase.1.schema.json deleted file mode 100644 index b43788f..0000000 --- a/schemas/testcase.1.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$id": "https://evidenceangel-schemas.hpkns.uk/testcase.1.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 type of data stored. Note that where `Http` is used, a Record Separator character (0x1e) can be used to split the request and response portion.", - "enum": ["Text", "RichText", "Image", "Http", "File"] - }, - "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"], - "if": { - "properties": { - "kind": { "const": "File" } - } - }, - "else": { - "not": { - "required": ["original_filename"] - } - } - } - } - }, - "required": ["metadata", "evidence"] -} diff --git a/src/exporters.rs b/src/exporters.rs deleted file mode 100644 index f32eedc..0000000 --- a/src/exporters.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::path::PathBuf; - -use uuid::Uuid; - -use crate::{EvidencePackage, Result}; - -/// Exporter for Excel files. -#[cfg(feature = "exporter-excel")] -pub mod excel; -/// Exporter for HTML. -#[cfg(feature = "exporter-html")] -pub mod html; -/// Exporter for a ZIP of the files. -#[cfg(feature = "exporter-zip-of-files")] -pub mod zip_of_files; - -/// Exporters can take an `EvidencePackage` and a target file path and export to other formats. -pub trait Exporter { - /// The name of this exporter. - fn export_name() -> String; - /// The file extension to suggest when saving this file. - fn export_extension() -> String; - - /// Export a package. - /// - /// # Errors - /// - /// Returns an error if the export failed for any reason. - fn export_package(&mut self, package: &mut EvidencePackage, path: PathBuf) -> Result<()>; - /// Export a test case. - /// - /// # Errors - /// - /// Returns an error if the export failed for any reason. - fn export_case( - &mut self, - package: &mut EvidencePackage, - case: Uuid, - path: PathBuf, - ) -> Result<()>; -} diff --git a/src/exporters/excel.rs b/src/exporters/excel.rs deleted file mode 100644 index 5da7d50..0000000 --- a/src/exporters/excel.rs +++ /dev/null @@ -1,536 +0,0 @@ -use angelmark::{AngelmarkLine, AngelmarkTableAlignment, AngelmarkText, parse_angelmark}; -use rust_xlsxwriter::{Format, FormatAlign, FormatBorder, Image, Note, Workbook, Worksheet}; -use uuid::Uuid; - -use crate::{EvidenceKind, EvidencePackage, TestCase, TestCasePassStatus}; - -use super::Exporter; - -/// An exporter to an Excel document. -#[derive(Default)] -pub struct ExcelExporter; - -impl Exporter for ExcelExporter { - fn export_name() -> String { - "Excel Workbook".to_string() - } - - fn export_extension() -> String { - ".xlsx".to_string() - } - - fn export_package( - &mut self, - package: &mut EvidencePackage, - path: std::path::PathBuf, - ) -> crate::Result<()> { - let mut workbook = Workbook::new(); - workbook.read_only_recommended(); - - create_metadata_sheet(workbook.add_worksheet(), package) - .map_err(crate::Error::OtherExportError)?; - - create_summary_sheet(workbook.add_worksheet(), package) - .map_err(crate::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)?; - } - - workbook - .save(path) - .map_err(|e| crate::Error::OtherExportError(e.into()))?; - - Ok(()) - } - - fn export_case( - &mut self, - package: &mut EvidencePackage, - case: Uuid, - path: std::path::PathBuf, - ) -> crate::Result<()> { - 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(), - ))?; - create_test_case_sheet(worksheet, package.clone(), case) - .map_err(crate::Error::OtherExportError)?; - - workbook - .save(path) - .map_err(|e| crate::Error::OtherExportError(e.into()))?; - - Ok(()) - } -} - -/// Create the worksheet for the metadata -fn create_metadata_sheet( - worksheet: &mut Worksheet, - package: &EvidencePackage, -) -> Result<(), Box> { - tracing::debug!("Creating excel sheet for metadata"); - worksheet.set_name(package.metadata().title())?; - worksheet.set_screen_gridlines(false); - worksheet.set_column_width(0, 3)?; // To appear tidy - - let mut row = 1; - - let title = Format::new().set_bold().set_font_size(14); - let italic = Format::new().set_italic(); - - // Write title and execution timestamp - worksheet.write_string_with_format(row, 1, package.metadata().title(), &title)?; - row += 1; - - for author in package.metadata().authors() { - row += 1; - worksheet.write_string_with_format(row, 1, format!("{author}"), &italic)?; - } - - row += 2; - if let Some(description) = package.metadata().description() { - worksheet.write_string(row, 1, description)?; - } - - if let Ok(branding_img) = std::env::var("EA_BRAND_IMAGE") { - row += 2; - let image = Image::new(branding_img)?; - worksheet.insert_image(row, 1, &image)?; - } - - Ok(()) -} - -/// Create the worksheet for the test case summary -fn create_summary_sheet( - worksheet: &mut Worksheet, - package: &EvidencePackage, -) -> Result<(), Box> { - tracing::debug!("Creating excel sheet for summary"); - worksheet.set_name("Summary")?; - worksheet.set_screen_gridlines(false); - worksheet.set_column_width(0, 3)?; // To appear tidy - - let mut row = 1; - - let title = Format::new().set_bold().set_font_size(14); - let bold_bordered = Format::new().set_bold().set_border(FormatBorder::Thin); - let bordered = Format::new().set_border(FormatBorder::Thin); - - // Write title and execution timestamp - worksheet.write_string_with_format(row, 1, "Summary", &title)?; - row += 2; - - // Write header row - worksheet.write_string_with_format(row, 1, "Test Case", &bold_bordered)?; - 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() { - let mut fields = fields.iter().collect::>(); - fields.sort_by(|(_, a), (_, b)| a.cmp(b)); - for (idx, (key, field)) in fields.iter().enumerate() { - let col = u16::try_from(4 + idx)?; - custom_keys.push((*key).clone()); - worksheet.write_string_with_format(row, col, field.name(), &bold_bordered)?; - if !field.description().is_empty() { - worksheet.insert_note(row, col, &Note::new(field.description()))?; - } - } - } - row += 1; - - // Write data rows - for test_case in package.test_case_iter()? { - worksheet.write_string_with_format(row, 1, test_case.metadata().title(), &bordered)?; - worksheet.write_string_with_format( - row, - 2, - test_case.metadata().execution_datetime().to_rfc3339(), - &bordered, - )?; - worksheet.write_string_with_format( - row, - 3, - match test_case.metadata().passed() { - None => "", - Some(TestCasePassStatus::Pass) => "Pass", - Some(TestCasePassStatus::Fail) => "Fail", - }, - &bordered, - )?; - 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)?; - } - } - } - row += 1; - } - worksheet.autofit(); - - Ok(()) -} - -/// Create the worksheet that holds the test case's information -fn create_test_case_sheet( - worksheet: &mut Worksheet, - mut package: EvidencePackage, - test_case: &TestCase, -) -> Result<(), Box> { - tracing::debug!("Creating excel sheet for test case {}", test_case.id()); - worksheet.set_name(test_case.metadata().title())?; - worksheet.set_screen_gridlines(false); - worksheet.set_column_width(0, 3)?; // To appear tidy - worksheet.set_column_width(1, 13)?; // For "Executed at:" - worksheet.set_column_width(2, 20)?; // For execution date/time - - let mut row = 1; - - let title = Format::new().set_bold().set_font_size(14); - let bold = Format::new().set_bold(); - let italic = Format::new().set_italic(); - let file_data = Format::new() - .set_font_name("Courier New") - .set_border_left(FormatBorder::Thick); - - // Write title and execution timestamp - worksheet.write_string_with_format(row, 1, test_case.metadata().title(), &title)?; - row += 1; - worksheet.write(row, 1, "Executed at:")?; - worksheet.write_with_format( - row, - 2, - &test_case.metadata().execution_datetime().naive_local(), - &Format::new().set_num_format("yyyy-mm-dd hh:mm"), - )?; - row += 1; - match test_case.metadata().passed() { - None => (), - Some(s) => { - let s = match s { - TestCasePassStatus::Pass => "✅ Pass", - TestCasePassStatus::Fail => "❌ Fail", - }; - worksheet.write(row, 1, s)?; - row += 1; - } - } - if let Some(fields) = test_case.metadata().custom() { - for (key, value) in fields { - let field = package - .metadata() - .custom_test_case_metadata() - .as_ref() - // SAFETY: guanteed by EVP spec - .unwrap() - .get(key) - // SAFETY: guanteed by EVP spec - .unwrap(); - worksheet.write(row, 1, format!("{}: {}", field.name(), value))?; - row += 1; - } - } - row += 1; - - // Write evidence - for evidence in test_case.evidence() { - if let Some(caption) = evidence.caption() { - worksheet.write_with_format(row, 1, caption, &italic)?; - row += 1; - } - - match evidence.kind() { - EvidenceKind::Text => { - let data = evidence.value().get_data(&mut package)?; - let text = String::from_utf8_lossy(data.as_slice()); - for line in text.lines() { - worksheet.write_string(row, 1, line)?; - row += 1; - } - } - EvidenceKind::RichText => { - 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(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; - } - } - } - } - } else { - for line in text.lines() { - worksheet.write_string_with_format( - row, - 1, - line, - &Format::default().set_font_name("Courier New"), - )?; - row += 1; - } - } - } - 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 => { - worksheet.write_string_with_format(row, 1, "HTTP Request", &bold)?; - row += 1; - let data = evidence.value().get_data(&mut package)?; - let text = String::from_utf8_lossy(data.as_slice()); - for line in text.lines() { - worksheet.write_string_with_format(row, 1, line, &file_data)?; - row += 1; - } - } - EvidenceKind::File => { - 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; - } - - // 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)?; - row += 1; - } - } else { - worksheet.write_string_with_format(row, 1, "binary file data", &italic)?; - row += 1; - } - } - } - - row += 1; - } - - 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")) - } - } -} diff --git a/src/exporters/html.css b/src/exporters/html.css deleted file mode 100644 index ea88d93..0000000 --- a/src/exporters/html.css +++ /dev/null @@ -1,175 +0,0 @@ -body { - font-family: "Segoe UI", "Liberation Sans", sans-serif; - margin: 8px auto; - max-width: 800px; - padding: 0 16px; -} - -img { - max-width: 100%; - display: block; - margin: 16px 0; -} - -hr { - border: none; - border-bottom: 1px solid black; -} - -pre { - border-left: 1px solid gray; - overflow-x: scroll; - padding-left: 8px; -} - -@media print { - pre { - overflow-x: auto; - text-wrap: wrap; - } -} - -table { - border-collapse: collapse; - margin: 8px 0; -} - -td { - border: 1px solid black; -} - -.authors, -.caption, -.execution-time { - font-style: italic; -} - -.title { - margin-bottom: 0; -} - -.execution-time, -.status, -.custom-metadata-fields { - margin-top: 2px; - margin-bottom: 4px; -} - -.metadata { - margin-bottom: 32px; - padding-bottom: 8px; - border-bottom: 1px solid lightgray; -} - -.tabs { - display: flex; - overflow-x: scroll; - padding: 0 16px; - border-bottom: 1px solid black; -} - -.tabs > li { - border-left: 1px solid black; - border-top: 1px solid black; - border-right: 1px solid black; - margin: 0; - margin-right: -1px; - list-style: none; - padding: 8px 6px; - text-wrap: nowrap; -} - -.tabs > li.selected { - font-weight: bold; -} - -.tabs > li > a { - color: black; - text-decoration: none; -} - -@media screen { - .show-all { - .tabs { - display: none; - } - - .tab-content { - display: block !important; - border-top: 1px solid black; - margin-top: 2rem; - } - } - - .tab-content.selected { - display: block; - } - - .tab-content { - display: none; - } - - .tab-content hr { - display: none; - } -} - -@media print { - .print-hide { - display: none; - } - - .tabs { - display: none; - } - - .tab-content { - border-top: 1px solid black; - margin-top: 2rem; - } -} - -.http-container { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.http-container > div { - width: calc(50% - 4px); -} - -@media screen and (max-width: 800px) { - .http-container > div { - width: 100%; - } -} - -.http-request:before { - content: "Request"; -} - -.http-response:before { - content: "Response"; -} - -.http-request:before, -.http-response:before { - font-variant: small-caps; -} - -.http-container pre { - padding-bottom: 10px; -} - -.richtext-bold { - font-weight: bold; -} - -.richtext-italic { - font-style: italic; -} - -.richtext-monospace { - font-family: 'Liberation Mono', 'Consolas', 'Courier New', Courier, monospace; -} diff --git a/src/exporters/html.js b/src/exporters/html.js deleted file mode 100644 index 37ba96e..0000000 --- a/src/exporters/html.js +++ /dev/null @@ -1,42 +0,0 @@ -let tabAnchorMatch = () => { - let tabRegex = /^#tab(\d+)$/; - let pageHash = window.location.hash; - - if (pageHash) { - let match = pageHash.match(tabRegex); - if (match && match[1]) { - // Found tab index - let tabIndex = parseInt(match[1]); - // Hide all tabs and remove selected from tab links - document - .querySelectorAll("[data-tab-index]") - .forEach((e) => e.classList.remove("selected")); - // Show tab by index - document - .querySelectorAll(`[data-tab-index='${tabIndex}']`) - .forEach((e) => e.classList.add("selected")); - } - } -}; - -window.addEventListener('DOMContentLoaded', () => { - const showAll = document.getElementById('showAll'); - const body = document.querySelector('body'); - const updateShowAll = () => { - if (showAll.checked) { - body.classList.add('show-all'); - } else { - body.classList.remove('show-all'); - } - }; - showAll.addEventListener('click', () => { - updateShowAll(); - }); - // And in case the page has some reloaded state - updateShowAll(); - - hljs.highlightAll(); -}); - -window.addEventListener("hashchange", tabAnchorMatch); -window.addEventListener("DOMContentLoaded", tabAnchorMatch); diff --git a/src/exporters/html.rs b/src/exporters/html.rs deleted file mode 100644 index 143cef7..0000000 --- a/src/exporters/html.rs +++ /dev/null @@ -1,480 +0,0 @@ -use std::fmt::Write; -use std::fs; - -use 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 super::Exporter; - -/// An exporter to HTML document. -#[derive(Default)] -pub struct HtmlExporter; - -impl Exporter for HtmlExporter { - fn export_name() -> String { - "HTML Document".to_string() - } - - fn export_extension() -> String { - ".html".to_string() - } - - fn export_package( - &mut self, - package: &mut EvidencePackage, - path: std::path::PathBuf, - ) -> crate::Result<()> { - let mut page = HtmlPage::new() - .with_title(html_escape::encode_text(package.metadata().title())) - .with_style(include_str!("html.css")) - .with_script_literal(include_str!("html.js")) - .with_stylesheet( - "https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/default.min.css", - ) - .with_script_link("https://unpkg.com/@highlightjs/cdn-assets@11.11.1/highlight.min.js") - .with_script_link( - "https://unpkg.com/@highlightjs/cdn-assets@11.11.1/languages/http.min.js", - ); - - let title = HtmlElement::new(HtmlTag::Heading1) - .with_raw(html_escape::encode_text(package.metadata().title())); - page.add_html(title); - - let mut authors = String::new(); - for author in package.metadata().authors() { - if let Some(email) = author.email() { - // SAFETY: This won't fail as there's no I/O - write!(authors, "{} <{}>, ", author.name(), email).unwrap(); - } else { - // SAFETY: This won't fail as there's no I/O - write!(authors, "{}, ", author.name()).unwrap(); - } - } - authors.pop(); - authors.pop(); - - page.add_html( - HtmlElement::new(HtmlTag::ParagraphText) - .with_attribute("class", "authors") - .with_raw(html_escape::encode_text(&authors)), - ); - - if let Some(description) = package.metadata().description() { - page.add_html( - HtmlElement::new(HtmlTag::ParagraphText) - .with_raw(html_escape::encode_text(description)), - ); - } - - if let Ok(branding_img) = std::env::var("EA_BRAND_IMAGE") { - let data = fs::read(branding_img).map_err(|e| { - std::io::Error::other(format!("Failed to read company brand image: {e}")) - })?; - let src = format!( - "data:application/octet-stream;base64,{}", - base64::prelude::BASE64_STANDARD_NO_PAD.encode(data) - ); - page.add_image( - src, - format!( - "{} Logo", - std::env::var("EA_BRAND_NAME").unwrap_or("Brand".to_string()) - ), - ); - } - - let test_cases: Vec<&TestCase> = package.test_case_iter()?.collect(); - let mut first = true; - let mut test_case_elems = vec![]; - let mut tab_container = - HtmlElement::new(HtmlTag::UnorderedList).with_attribute("class", "tabs"); - for (idx, test_case) in test_cases.iter().enumerate() { - let mut tab_elem = HtmlElement::new(HtmlTag::ListElement) - .with_attribute("data-tab-index", idx) - .with_link( - format!("#tab{idx}"), - format!( - "{}{}", - match test_case.metadata().passed() { - None => "", - Some(TestCasePassStatus::Pass) => "✅ ", - Some(TestCasePassStatus::Fail) => "❌ ", - }, - test_case.metadata().title() - ), - ); - if first { - tab_elem.add_attribute("class", "selected"); - } - tab_container.add_html(tab_elem); - - let elem = create_test_case_div(package.clone(), test_case) - .map_err(crate::Error::OtherExportError)? - .with_attribute("data-tab-index", idx) - .with_attribute( - "class", - format!( - "tab-content {}", - if first { - first = false; - "selected" - } else { - "" - } - ), - ); - test_case_elems.push(elem); - } - page.add_raw(r#""#); - page.add_html(tab_container); - for elem in test_case_elems { - page.add_html(elem); - } - - fs::write(path, page.to_html_string())?; - - Ok(()) - } - - fn export_case( - &mut self, - package: &mut EvidencePackage, - case: Uuid, - path: std::path::PathBuf, - ) -> crate::Result<()> { - let mut page = HtmlPage::new() - .with_title(html_escape::encode_text(package.metadata().title())) - .with_style(include_str!("html.css")) - .with_script_literal(include_str!("html.js")); - - 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)?; - page.add_html(elem); - - fs::write(path, page.to_html_string())?; - - Ok(()) - } -} - -/// Create the
element that holds a test case's data -fn create_test_case_div( - mut package: EvidencePackage, - test_case: &TestCase, -) -> Result> { - tracing::debug!("Creating HTML element for test case {}", test_case.id()); - let mut elem = HtmlElement::new(HtmlTag::Div); - let mut meta_elem = HtmlElement::new(HtmlTag::Div) - .with_attribute("class", "metadata") - .with_html( - HtmlElement::new(HtmlTag::Heading2) - .with_attribute("class", "title") - .with_raw(html_escape::encode_text(test_case.metadata().title())), - ) - .with_html( - HtmlElement::new(HtmlTag::ParagraphText) - .with_attribute("class", "execution-time") - .with_raw(test_case.metadata().execution_datetime().to_rfc2822()), - ); - match test_case.metadata().passed() { - None => (), - Some(s) => { - let s = match s { - TestCasePassStatus::Pass => "✅ Pass", - TestCasePassStatus::Fail => "❌ Fail", - }; - meta_elem.add_html( - HtmlElement::new(HtmlTag::ParagraphText) - .with_attribute("class", "status") - .with_raw(s), - ); - } - } - if let Some(fields) = test_case.metadata().custom() { - let mut dl = HtmlElement::new(HtmlTag::DescriptionList) - .with_attribute("class", "custom-metadata-fields"); - for (key, value) in fields { - let field = package - .metadata() - .custom_test_case_metadata() - .as_ref() - // SAFETY: guanteed by EVP spec - .unwrap() - .get(key) - // SAFETY: guanteed by EVP spec - .unwrap(); - dl.add_html( - HtmlElement::new(HtmlTag::DescriptionListTerm) - .with_attribute("class", "custom-metadata-field-name") - .with_raw(field.name()), - ); - dl.add_html( - HtmlElement::new(HtmlTag::DescriptionListDescription) - .with_attribute("class", "custom-metadata-field-value") - .with_raw(value), - ); - } - meta_elem.add_html(dl); - } - elem.add_html(meta_elem); - - // Write evidence - for evidence in test_case.evidence() { - if let Some(caption) = evidence.caption() { - elem.add_html( - HtmlElement::new(HtmlTag::ParagraphText) - .with_attribute("class", "caption") - .with_raw(html_escape::encode_text(caption)), - ); - } - - match evidence.kind() { - EvidenceKind::Text => { - let data = evidence.value().get_data(&mut package)?; - let text = String::from_utf8_lossy(data.as_slice()); - for line in text.lines() { - elem.add_html( - HtmlElement::new(HtmlTag::ParagraphText) - .with_raw(html_escape::encode_text(line)), - ); - } - } - EvidenceKind::RichText => { - 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}")), - ); - } - } - EvidenceKind::Http => { - let data = evidence.value().get_data(&mut package)?; - let data = String::from_utf8_lossy(data.as_slice()); - let data_parts = data - .split('\x1e') - .map(std::string::ToString::to_string) - .collect::>(); - let request = data_parts.first().cloned().unwrap_or_default(); - let response = data_parts.get(1).cloned().unwrap_or_default(); - - elem.add_html( - HtmlElement::new(HtmlTag::Div) - .with_attribute("class", "http-container") - .with_html( - HtmlElement::new(HtmlTag::Div) - .with_attribute("class", "http-request") - .with_html( - HtmlElement::new(HtmlTag::PreformattedText).with_html( - HtmlElement::new(HtmlTag::CodeText) - .with_attribute("class", "language-http") - .with_raw(html_escape::encode_text(&request)), - ), - ), - ) - .with_html( - HtmlElement::new(HtmlTag::Div) - .with_attribute("class", "http-response") - .with_html( - HtmlElement::new(HtmlTag::PreformattedText).with_html( - HtmlElement::new(HtmlTag::CodeText) - .with_attribute("class", "language-http") - .with_raw(html_escape::encode_text(&response)), - ), - ), - ), - ); - } - 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() - } 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() - }), - ), - ); - } - } - } - - 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 deleted file mode 100644 index cb013cb..0000000 --- a/src/exporters/zip_of_files.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::{ - collections::HashMap, - fs, - io::{self, BufWriter, Cursor}, -}; - -use thiserror::Error; -use uuid::Uuid; -use zip::{ZipWriter, write::SimpleFileOptions}; - -use crate::{EvidenceKind, EvidencePackage, TestCase}; - -use super::Exporter; - -/// An exporter to an ZIP of the files in the package. -#[derive(Default)] -pub struct ZipOfFilesExporter; - -/// An error from the [`ZipOfFilesExporter`]. -#[derive(Debug, Error)] -pub enum ZipOfFilesError { - /// No files are in this package, so no output would be produced - #[error("no files are in this package, so no output would be produced")] - NoFilesToExport, -} - -impl Exporter for ZipOfFilesExporter { - fn export_name() -> String { - "ZIP Archive of Files".to_string() - } - - fn export_extension() -> String { - ".zip".to_string() - } - - fn export_package( - &mut self, - package: &mut EvidencePackage, - path: std::path::PathBuf, - ) -> crate::Result<()> { - fn safely_add_cases_to_zip( - mut zip: ZipWriter>, - package: &mut EvidencePackage, - ) -> crate::Result<()> { - for test_case in package.test_case_iter()? { - add_test_case_to_zip(&mut zip, package.clone(), test_case) - .map_err(crate::Error::OtherExportError)?; - } - - zip.finish() - .map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; - - Ok(()) - } - - let mut has_files = false; - for test_case in package.test_case_iter()? { - if check_has_files(test_case) { - has_files = true; - break; - } - } - if !has_files { - return Err(crate::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)))?, - )); - if let Err(e) = safely_add_cases_to_zip(zip, package) { - // Delete file if exists - let _ = fs::remove_file(path); - - return Err(e); - } - - Ok(()) - } - - fn export_case( - &mut self, - package: &mut EvidencePackage, - case: Uuid, - path: std::path::PathBuf, - ) -> crate::Result<()> { - fn inner( - mut zip: ZipWriter>, - package: &mut EvidencePackage, - case: &TestCase, - ) -> crate::Result<()> { - add_test_case_to_zip(&mut zip, package.clone(), case) - .map_err(crate::Error::OtherExportError)?; - - zip.finish() - .map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; - - Ok(()) - } - - let case = package - .test_case(case)? - .ok_or(crate::Error::OtherExportError( - "Test case not found!".into(), - ))? - .clone(); - - if !check_has_files(&case) { - return Err(crate::Error::OtherExportError(Box::new( - ZipOfFilesError::NoFilesToExport, - ))); - } - - let file = - fs::File::create(&path).map_err(|e| crate::Error::OtherExportError(Box::new(e)))?; - let zip = ZipWriter::new(BufWriter::new(file)); - if let Err(e) = inner(zip, package, &case) { - // Delete file if exists - let _ = fs::remove_file(path); - - return Err(e); - } - - Ok(()) - } -} - -/// 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() { - return true; - } - } - false -} - -/// Create the worksheet that holds the test case's information -fn add_test_case_to_zip( - zip: &mut ZipWriter>, - mut package: EvidencePackage, - test_case: &TestCase, -) -> Result<(), Box> { - tracing::debug!("Creating ZIP of files for test case {}", test_case.id()); - - let mut filename_count = HashMap::new(); - for ev in test_case.evidence() { - if let EvidenceKind::File = ev.kind() { - if let Some(filename) = ev.original_filename() { - filename_count.insert(filename, filename_count.get(filename).unwrap_or(&0) + 1); - } - } - } - - let mut unnamed_counter = 0; - - // Write evidence - for evidence in test_case.evidence() { - if let EvidenceKind::File = evidence.kind() { - 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() { - hash.clone() - } else { - unnamed_counter += 1; - format!("unnamed-{unnamed_counter}") - }; - - let disambiguator = if let Some(count) = filename_count.get(&name) { - if *count > 1 { - if let Some(caption) = evidence.caption() { - format!("({caption}) ") - } else { - String::new() - } - } else { - String::new() - } - } else { - String::new() - }; - - // Add to ZIP file - zip.start_file( - format!("{}/{disambiguator}{name}", test_case.metadata().title()), - SimpleFileOptions::default(), - ) - .map_err(|e| crate::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)))?; - } - } - - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index dddf4d7..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![deny(missing_docs)] -#![deny(clippy::missing_docs_in_private_items)] -#![deny(unsafe_code)] -#![warn(clippy::pedantic)] -#![allow(clippy::too_many_lines)] -#![allow(clippy::cast_possible_truncation)] -#![allow(clippy::cast_sign_loss)] - -//! # `EvidenceAngel` -//! -//! `EvidenceAngel` is a new tool in the Angel-suite to collect test evidence -//! from both manual and automated testing. - -/// 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, -}; -/// 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; -/// Open a ZIP file in a fashion that allows it to be switched between reading and writing. -mod zip_read_writer; diff --git a/src/lock_file.rs b/src/lock_file.rs deleted file mode 100644 index 57d7511..0000000 --- a/src/lock_file.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::{ - fs, - io::{self, Write}, - path, process, -}; - -/// A locking file -pub struct LockFile { - /// The path of this lockfile - path: path::PathBuf, - /// The file handle. - file: fs::File, -} - -impl LockFile { - /// Create a new lock file that is released when this is dropped. - /// - /// # Errors - /// - /// If this returns an error of any kind, it should be assumed that - /// a lock could not be obtained. - pub fn new

(path: P) -> io::Result - where - P: AsRef, - { - Ok(LockFile { - path: path.as_ref().to_path_buf(), - file: Self::create_file_handle(path)?, - }) - } - - /// Actually create the file handle - fn create_file_handle

(path: P) -> io::Result - where - P: AsRef, - { - let mut file = fs::File::create_new(&path)?; - file.write_all(process::id().to_string().as_bytes())?; - file.flush()?; - - #[allow(unsafe_code)] - #[cfg(windows)] - { - use std::os::windows::ffi::OsStrExt; - use winapi::um::fileapi::SetFileAttributesW; - use winapi::um::winnt::FILE_ATTRIBUTE_HIDDEN; - - let path = path.as_ref(); - let wide_path: Vec = path - .as_os_str() - .encode_wide() - .chain(std::iter::once(0)) - .collect(); - let _res = unsafe { SetFileAttributesW(wide_path.as_ptr(), FILE_ATTRIBUTE_HIDDEN) }; - } - - Ok(file) - } - - /// Check that the lock is still in place, recreating it if needed. - /// - /// # Errors - /// - /// A failure here indicates that the lock is no longer in place, - /// and cannot be replaced. This might be as a result of another - /// user deleting the lock file and obtaining their own. You should - /// handle this with caution. - pub fn ensure_still_locked(&mut self) -> io::Result<()> { - if fs::exists(&self.path)? { - Ok(()) - } else { - // Need to reobtain file lock - self.file = Self::create_file_handle(&self.path)?; - Ok(()) - } - } -} - -impl Drop for LockFile { - fn drop(&mut self) { - if let Err(e) = fs::remove_file(&self.path) { - tracing::warn!("Failed to delete lock file: {e}"); - } - } -} diff --git a/src/package.rs b/src/package.rs deleted file mode 100644 index 10611dc..0000000 --- a/src/package.rs +++ /dev/null @@ -1,661 +0,0 @@ -use std::{ - collections::HashMap, - fmt, - io::{self, BufReader, Read, Write}, - path::PathBuf, -}; - -use chrono::{DateTime, FixedOffset, Local}; -use getset::{Getters, MutGetters}; -use serde::{Deserialize, Serialize}; -use test_cases::TESTCASE_SCHEMA; -use uuid::Uuid; -use zip::{result::ZipError, write::SimpleFileOptions}; - -use crate::{Result, result::Error, zip_read_writer::ZipReaderWriter}; - -/// Package manifests -mod manifest; -pub use manifest::*; - -/// Media handling -mod media; -pub use media::MediaFile; - -/// Test cases from packages -mod test_cases; -pub use test_cases::{ - Evidence, EvidenceData, EvidenceKind, TestCase, TestCaseMetadata, TestCasePassStatus, -}; - -/// The URL for $schema for manifest.json -const MANIFEST_SCHEMA_LOCATION: &str = - "https://evidenceangel-schemas.hpkns.uk/manifest.2.schema.json"; -/// The schema to validate manifest.json against -const MANIFEST_SCHEMA: &str = include_str!("../schemas/manifest.2.schema.json"); -/// The version 1 schema to validate manifest.json against -const MANIFEST_SCHEMA_V1: &str = include_str!("../schemas/manifest.1.schema.json"); - -/// An Evidence Package. -#[derive(Serialize, Deserialize, Getters, MutGetters)] -pub struct EvidencePackage { - /// The internal ZIP file. This will never be `None`, as long as it has been correctly parsed. - #[serde(skip)] - zip: ZipReaderWriter, - /// The actual media data from this package - #[serde(skip)] - media_data: HashMap, - /// The actual test data from this package - #[serde(skip)] - test_case_data: HashMap, - - /// The JSON schema for for this package - #[serde(rename = "$schema")] - schema: Option, - /// The metadata for the package. - #[getset(get = "pub", get_mut = "pub")] - metadata: Metadata, - /// The manifest entries for the media in this package - media: Vec, - /// The manifest entries for the test cases in this package - test_cases: Vec, - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl Clone for EvidencePackage { - fn clone(&self) -> Self { - Self { - zip: self.zip.clone(), - media_data: HashMap::new(), - test_case_data: self.test_case_data.clone(), - extra_fields: HashMap::new(), - - schema: Some(MANIFEST_SCHEMA_LOCATION.to_string()), - metadata: self.metadata.clone(), - media: self.media.clone(), - test_cases: self.test_cases.clone(), - } - } -} - -impl fmt::Debug for EvidencePackage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("EvidencePackage") - .field("metadata", &self.metadata) - .field("media", &self.media) - .field("test_cases", &self.test_cases) - .field("extra_fields", &self.extra_fields) - .finish_non_exhaustive() - } -} - -impl EvidencePackage { - /// Create a new evidence package. - /// - /// # Errors - /// - /// - [`Error::Io`] if the evp couldn't be written at all. - /// - [`Error::Zip`] if the evp file couldn't be written correctly. - /// - [`Error::ManifestSchemaValidationFailed`] if the manifest is invalid. - #[allow( - clippy::missing_panics_doc, - reason = "panics have been statically validated to never occur" - )] - pub fn new(path: PathBuf, title: String, authors: Vec) -> Result { - Self::new_with_description(path, title, None, authors) - } - - /// Create a new evidence package with a specified description. - /// - /// # Errors - /// - /// - [`Error::Io`] if the evp couldn't be written at all. - /// - [`Error::Zip`] if the evp file couldn't be written correctly. - /// - [`Error::ManifestSchemaValidationFailed`] if the manifest is invalid. - #[allow( - clippy::missing_panics_doc, - reason = "panics have been statically validated to never occur" - )] - pub fn new_with_description( - path: PathBuf, - title: String, - description: Option, - authors: Vec, - ) -> Result { - // Create manifest data. - let mut manifest = Self { - zip: ZipReaderWriter::new(path)?, - media_data: HashMap::new(), - test_case_data: HashMap::new(), - - schema: Some(MANIFEST_SCHEMA_LOCATION.to_string()), - media: vec![], - test_cases: vec![], - metadata: Metadata { - title, - description, - authors, - custom_test_case_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)?; - - 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())?; - manifest.zip.conclude_write()?; - - Ok(manifest) - } - - /// Save the package to disk. - /// - /// # Panics - /// - /// All the potential panics are checked statically ahead of time, so should never trigger at runtime. - /// - /// # Errors - /// - /// - [`Error::Io`] if the evp couldn't be written at all. - /// - [`Error::Zip`] if the evp file couldn't be written correctly. - /// - [`Error::FailedToSaveTestCase`] if one of the test cases couldn't be saved. - /// - [`Error::TestCaseSchemaValidationFailed`] if one of the test case manifests fails schema validation after saving. - /// - [`Error::MediaMissing`] if the package is missing media required to be saved. - /// - [`Error::FailedToCreatePackage`] if the package manifest couldn't be saved. - /// - [`Error::ManifestSchemaValidationFailed`] if the manifest is invalid. - pub fn save(&mut self) -> Result<()> { - let mut clone = self.clone_serde(); - { - // 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 (mut maybe_old_archive, zip) = self.zip.as_writer()?; - let options = SimpleFileOptions::default(); - - // Create empty structure. - zip.add_directory("media", options)?; - zip.add_directory("testcases", options)?; - - tracing::trace!("Current media cache: {:?}", self.media_data); - - let mut media_used = vec![]; - - // Write any files as needed - for test_case in &self.test_cases { - let id = test_case.id(); - if let Some(data) = self.test_case_data.get(id) { - // Whilst we are here, let's figure out what media we use. - for evidence in data.evidence() { - if let EvidenceData::Media { hash } = evidence.value() { - media_used.push(hash); - } - } - - let data = serde_json::to_string(data) - .map_err(crate::result::Error::FailedToSaveTestCase)?; - 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(); - return Err(Error::TestCaseSchemaValidationFailed); - } - zip.start_file(format!("testcases/{id}.json"), options)?; - zip.write_all(data.as_bytes())?; - } - } - - // Scrub unused media manifest entries - self.media - .retain(|entry| media_used.contains(&entry.sha256_checksum())); - clone - .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)); - - // 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 in memory, write from there - tracing::trace!("Writing from cache {hash}"); - zip.write_all(self.media_data.get(hash).unwrap().data())?; - } else { - // Otherwise pull from previous package. - // Consider moving this to not load entire file on move. - if maybe_old_archive.is_some() { - tracing::debug!("Migrating media with hash {hash} from old file"); - let old_archive = maybe_old_archive.as_mut().unwrap(); - let res = old_archive.by_name(&format!("media/{hash}")); - match res { - Err(ZipError::FileNotFound) => { - return Err(Error::MediaMissing(hash.clone())); - } - Err(e) => { - tracing::error!("Error migrating from old package: {e}"); - return Err(e.into()); - } - Ok(mut file) => { - io::copy(&mut file, zip)?; - } - } - } else { - unreachable!(); - } - } - } - - // 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)?; - 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(); - return Err(Error::ManifestSchemaValidationFailed); - } - zip.start_file("manifest.json", options)?; - zip.write_all(manifest_data.as_bytes())?; - self.zip.conclude_write()?; - Ok(()) - } - - /// Open an evidence package, returning either the parsed evidence package for manipulation, or an error. - /// - /// # Panics - /// - /// All the potential panics are checked statically ahead of time, so should never trigger at runtime. - /// - /// # Errors - /// - /// - [`Error::Io`] if the evp couldn't be read at all. - /// - [`Error::Zip`] if the evp file couldn't be read correctly. - /// - [`Error::CorruptEvidencePackage`] if the evp is corrupt in it's internal structure. - /// - [`Error::ManifestSchemaValidationFailed`] if the manifest is invalid. - /// - [`Error::InvalidManifest`] if the manifest passes schema validation but is somehow still invalid. - /// - [`Error::TestCaseSchemaValidationFailed`] if one of the test case manifests fails schema validation. - pub fn open(path: PathBuf) -> Result { - // Open ZIP file - let mut zip_rw = ZipReaderWriter::new(path)?; - let zip = zip_rw.as_reader()?; - - // Read manifest - let manifest_entry = zip - .by_name("manifest.json") - .map_err(|_| Error::CorruptEvidencePackage("missing manifest".to_string()))?; - let manifest_data = { - let mut buf_manifest = BufReader::new(manifest_entry); - let mut manifest_data = String::new(); - buf_manifest.read_to_string(&mut manifest_data)?; - manifest_data - }; - - // Validate manifest - if !jsonschema::is_valid( - &serde_json::from_str(MANIFEST_SCHEMA).expect("Schema is validated statically"), - &serde_json::from_str(&manifest_data) - .map_err(|_| Error::ManifestSchemaValidationFailed)?, - ) { - // Check if v1 matches - if jsonschema::is_valid( - &serde_json::from_str(MANIFEST_SCHEMA_V1).expect("Schema is validated statically"), - &serde_json::from_str(&manifest_data) - .map_err(|_| Error::ManifestSchemaValidationFailed)?, - ) { - // Upgrade to v2 by changing "name" to "id" will be performed by serde - tracing::debug!("Upgrade will happen for manifest to version 2"); - } else { - return Err(Error::ManifestSchemaValidationFailed); - } - } - - // Parse manifest - let mut evidence_package: EvidencePackage = - serde_json::from_str(&manifest_data).map_err(Error::InvalidManifest)?; - - // Read test cases - for test_case in &evidence_package.test_cases { - let id = test_case.id(); - let data = zip - .by_name(&format!("testcases/{id}.json")) - .map_err(|_| Error::CorruptEvidencePackage(format!("missing test case {id}")))?; - let test_case_data = { - let mut buf_test_case = BufReader::new(data); - let mut test_case_data = String::new(); - buf_test_case.read_to_string(&mut test_case_data)?; - test_case_data - }; - - // Validate test case against schema - if jsonschema::is_valid( - &serde_json::from_str(TESTCASE_SCHEMA).expect("Schema is validated statically"), - &serde_json::from_str(&test_case_data) - .map_err(|_| Error::TestCaseSchemaValidationFailed)?, - ) { - // Read as version 1 - tracing::debug!("Test case {id} opened as version 1"); - let mut test_case: TestCase = serde_json::from_str(&test_case_data) - .map_err(|e| Error::InvalidTestCase(e, *id))?; - test_case.set_id(*id); - test_case.update_schema(); - evidence_package.test_case_data.insert(*id, test_case); - } else { - return Err(Error::TestCaseSchemaValidationFailed); - } - } - - evidence_package.zip = 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(), - test_case_data: HashMap::new(), - - schema: Some(MANIFEST_SCHEMA_LOCATION.to_string()), - metadata: self.metadata.clone(), - media: self.media.clone(), - test_cases: self.test_cases.clone(), - extra_fields: HashMap::new(), - } - } - - /// Obtain an iterator over test cases in the order they are set. - /// - /// # Errors - /// - /// Currently cannot fail. - #[allow(clippy::missing_panics_doc)] - pub fn test_case_iter(&self) -> Result> { - Ok(self - .test_cases - .iter() - .map(|tcme| self.test_case(*tcme.id()).unwrap().unwrap())) - } - - /// Obtain an iterator over test cases. - /// Note that this is unsorted. - /// - /// # Errors - /// - /// Currently cannot fail. - pub fn test_case_iter_mut(&mut self) -> Result> { - Ok(self.test_case_data.values_mut()) - } - - /// Update the order of test cases - /// - /// # Panics - /// - /// Panics if the new order doesn't contain every test case in this package. - /// - /// # Errors - /// - /// Currently cannot fail. - pub fn set_test_case_order(&mut self, new_order: Vec) -> Result<()> { - let required_uuids = self - .test_cases - .iter() - .map(|tcme| *tcme.id()) - .collect::>(); - for uuid in required_uuids { - assert!( - new_order.contains(&uuid), - "The new order doesn't contain every test case in this package" - ); - } - - tracing::debug!("Committing new test case order: {new_order:?}"); - self.test_cases.clear(); - for uuid in new_order { - self.test_cases.push(TestCaseManifestEntry::new(uuid)); - } - - Ok(()) - } - - /// Create a new test case. - /// - /// # Errors - /// - /// Currently cannot fail. - #[allow(clippy::missing_panics_doc)] - pub fn create_test_case(&mut self, title: S) -> Result<&mut TestCase> - where - S: Into, - { - self.create_test_case_at(title, Local::now().fixed_offset()) - } - - /// Create a new test case at a specified time. - /// - /// # Errors - /// - /// Currently cannot fail. - #[allow(clippy::missing_panics_doc)] - pub fn create_test_case_at( - &mut self, - title: S, - at: DateTime, - ) -> Result<&mut TestCase> - where - S: Into, - { - let new_id = uuid::Uuid::new_v4(); - - // Create new manifest entry - self.test_cases.push(TestCaseManifestEntry::new(new_id)); - - // Create test case - self.test_case_data - .insert(new_id, TestCase::new(new_id, title.into(), at)); - - Ok(self.test_case_data.get_mut(&new_id).unwrap()) - } - - /// Create a new test case as a duplicate of another. - /// - /// # Errors - /// - /// - [`Error::DoesntExist`] the test case to duplicate doesn't exist. - #[allow(clippy::missing_panics_doc)] - pub fn duplicate_test_case(&mut self, case_id_to_duplicate: Uuid) -> Result<&mut TestCase> { - let case = self - .test_case(case_id_to_duplicate)? - .cloned() - .ok_or(Error::DoesntExist(case_id_to_duplicate))?; - let mut new_case = case.clone(); - let new_id = Uuid::new_v4(); - new_case.set_id(new_id); - - // Create new manifest entry - self.test_cases.push(TestCaseManifestEntry::new(new_id)); - - // Create test case - self.test_case_data.insert(new_id, new_case); - - Ok(self.test_case_data.get_mut(&new_id).unwrap()) - } - - /// Delete a test case. - /// Returns true if a case was deleted. - /// - /// # Errors - /// - /// Currently cannot fail. - #[allow(clippy::missing_panics_doc)] - pub fn delete_test_case(&mut self, id: U) -> Result - where - U: Into, - { - let id = id.into(); - - // Search for matching case - let index = self.test_cases.iter().position(|tc| *tc.id() == id); - - // Check a case was found - if index.is_none() { - return Ok(false); - } - - // Remove case and data object - let case = self.test_cases.remove(index.unwrap()); - self.test_case_data.remove(case.id()); - Ok(true) - } - - /// Get a test case - /// - /// # Errors - /// - /// Currently cannot fail. - pub fn test_case(&self, id: U) -> Result> - where - U: Into, - { - let id = id.into(); - - // Search for matching case - let case = self.test_cases.iter().find(|tc| *tc.id() == id); - - // Return case - Ok(case.and_then(|tcme| self.test_case_data.get(tcme.id()))) - } - - /// Mutably get a test case - /// - /// # Errors - /// - /// Currently cannot fail. - pub fn test_case_mut(&mut self, id: U) -> Result> - where - U: Into, - { - let id = id.into(); - - // Search for matching case - let case = self.test_cases.iter().find(|tc| *tc.id() == id); - - // Return case - Ok(case.and_then(|tcme| self.test_case_data.get_mut(tcme.id()))) - } - - /// Add media to this package. - /// - /// Media is automatically removed if it is not referenced when [`EvidencePackage::save`] is called. - /// As such, you can delete media simply by removing references to it. - /// - /// Media will remain in memory until [`EvidencePackage::save`] is called, at which point it will be - /// written to disk. It will remain cached in memory until the [`EvidencePackage`] is dropped. - /// - /// # Errors - /// - /// - [`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> { - let hash = media_file.hash(); - - if !self - .media - .iter() - .any(|entry| entry.sha256_checksum() == &hash) - { - // Create manifest entry - let manifest_entry = MediaFileManifestEntry::from(&media_file); - self.media.push(manifest_entry); - - // Insert data and return reference - tracing::trace!("New media cache entry: {hash}"); - self.media_data.insert(hash.clone(), media_file); - } - - Ok(self.get_media(&hash)?.unwrap()) - } - - /// Get media from this package by a sha256 hash. - /// - /// The in-memory cache will be searched first, then the file will be read again to pull the media. - /// - /// Returns [`None`] if the media couldn't be found with that hash. - /// - /// # Errors - /// - /// - [`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> - where - S: Into, - { - let hash = hash.into(); - - // Check in-memory cache - if self.media_data.contains_key(&hash) { - tracing::debug!("{hash} found in cache."); - return Ok(self.media_data.get(&hash)); - } - - // Read from ZIP file - let zip = self.zip.as_reader()?; - let res = zip.by_name(&format!("media/{}", hash.clone())); - match res { - Ok(file) => { - let size = file.size() as usize; - let mut buf = Vec::with_capacity(size); - tracing::debug!("Cache miss: {hash} (size: {size})"); - - // Read from ZIP data - let mut data = BufReader::new(file); - data.read_to_end(&mut buf)?; - - // Add to in-memory cache - let media: MediaFile = buf.into(); - tracing::trace!("New media cache entry: {hash}"); - self.media_data.insert(hash.clone(), media); - - // Return cached version - Ok(Some(self.media_data.get(&hash).unwrap())) - } - Err(ZipError::FileNotFound) => { - tracing::warn!("{hash} not found in package!"); - Ok(None) - } - Err(e) => Err(e.into()), - } - } -} diff --git a/src/package/manifest.rs b/src/package/manifest.rs deleted file mode 100644 index d722e01..0000000 --- a/src/package/manifest.rs +++ /dev/null @@ -1,208 +0,0 @@ -use getset::{Getters, MutGetters, Setters}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use std::{collections::HashMap, fmt}; - -/// [`EvidencePackage`](super::EvidencePackage) metadata. -#[derive(Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)] -#[getset(get = "pub", set = "pub")] -pub struct Metadata { - /// The package title. - pub(super) title: String, - - /// The package description. - #[serde(skip_serializing_if = "Option::is_none")] - pub(super) description: Option, - - /// The package authors. - #[get_mut = "pub"] - pub(super) authors: Vec, - - /// Custom metadata fields for test cases - #[serde(skip_serializing_if = "Option::is_none")] - #[allow( - clippy::struct_field_names, - reason = "This field refers to the name of it's subtype" - )] - pub(super) custom_test_case_metadata: Option>, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - pub(super) extra_fields: HashMap, -} - -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()); - } - // SAFETY: just initialised if wasn't previously - self.custom_test_case_metadata.as_mut().unwrap() - } - - /// Create a new custom metadata field - pub fn insert_custom_metadata_field( - &mut self, - id: Option, - name: String, - description: String, - make_primary: bool, - ) -> (String, CustomMetadataField) { - let custom_fields = self.custom_test_case_metadata_mut(); - - if make_primary { - // Make all other fields not primary - custom_fields - .iter_mut() - .for_each(|(_key, item)| item.primary = false); - } - - let new_id = id.unwrap_or_else(|| Uuid::new_v4().to_string()); - let field = CustomMetadataField { - name, - description, - primary: make_primary, - extra_fields: HashMap::new(), - }; - custom_fields.insert(new_id.clone(), field.clone()); - (new_id, field) - } -} - -/// An author of an [`EvidencePackage`](super::EvidencePackage). -#[derive(Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize, PartialEq, Eq)] -#[getset(get = "pub", get_mut = "pub", set = "pub")] -pub struct Author { - /// The author's name. - name: String, - /// The author's email address. - #[serde(skip_serializing_if = "Option::is_none")] - email: Option, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl Author { - /// Create a new author from a name. - pub fn new>(name: S) -> Self { - Self { - name: name.into(), - email: None, - extra_fields: HashMap::new(), - } - } - - /// Create a new author from a name and email address. - pub fn new_with_email>(name: S, email_address: S) -> Self { - Self { - name: name.into(), - email: Some(email_address.into()), - extra_fields: HashMap::new(), - } - } -} - -impl fmt::Display for Author { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.email.is_some() { - write!(f, "{} <{}>", self.name, self.email.as_ref().unwrap()) - } else { - write!(f, "{}", self.name) - } - } -} - -/// 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")] -pub struct CustomMetadataField { - /// A user-friendly name for this custom property. - name: String, - /// A description for this custom property. - description: String, - /// Is this custom property the main one in this package? - primary: bool, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl PartialOrd for CustomMetadataField { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for CustomMetadataField { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - if self.primary && !other.primary { - return std::cmp::Ordering::Less; - } - if !self.primary && other.primary { - return std::cmp::Ordering::Greater; - } - self.name.cmp(&other.name) - } -} - -/// The manifest entry for a media file present in the package. -#[derive(Clone, Debug, Getters, Serialize, Deserialize)] -#[getset(get = "pub")] -pub(super) struct MediaFileManifestEntry { - /// The SHA256 checksum of the media file. - sha256_checksum: String, - /// The MIME type of the media file. - mime_type: String, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl From<&crate::MediaFile> for MediaFileManifestEntry { - fn from(value: &crate::MediaFile) -> Self { - Self { - sha256_checksum: value.hash(), - mime_type: value - .mime_type() - .map_or("unknown", |t| t.mime_type()) - .to_string(), - extra_fields: HashMap::new(), - } - } -} - -/// An entry in the manifest storing the [`Uuid`] for a test case present in the -/// package. -#[derive(Clone, Debug, Getters, Serialize, Deserialize)] -#[getset(get = "pub")] -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, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl TestCaseManifestEntry { - /// Create a new test case manifest entry - pub(super) fn new(id: Uuid) -> Self { - Self { - id, - extra_fields: HashMap::new(), - } - } -} diff --git a/src/package/media.rs b/src/package/media.rs deleted file mode 100644 index 4572447..0000000 --- a/src/package/media.rs +++ /dev/null @@ -1,36 +0,0 @@ -use getset::Getters; -use std::fmt; - -/// A media file stored within an [`EvidencePackage`](super::EvidencePackage). -#[derive(Clone, Getters)] -pub struct MediaFile { - /// The raw data of this media file. - #[getset(get = "pub")] - data: Vec, -} - -impl fmt::Debug for MediaFile { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "MediaFile ({} bytes)", self.data.len()) - } -} - -impl MediaFile { - /// Generate a SHA256 hash of this data. - #[must_use] - pub fn hash(&self) -> String { - sha256::digest(&self.data) - } - - /// Determine the MIME type of this data. - #[must_use] - pub fn mime_type(&self) -> Option { - infer::get(&self.data) - } -} - -impl>> From for MediaFile { - fn from(value: D) -> Self { - Self { data: value.into() } - } -} diff --git a/src/package/test_cases.rs b/src/package/test_cases.rs deleted file mode 100644 index 23ebdcd..0000000 --- a/src/package/test_cases.rs +++ /dev/null @@ -1,313 +0,0 @@ -use std::collections::HashMap; - -use base64::Engine; -use chrono::{DateTime, FixedOffset}; -use getset::{Getters, MutGetters, Setters}; -use serde::{ - Deserialize, Serialize, - de::{self, Visitor}, -}; -use uuid::Uuid; - -/// The URL for $schema in the test case manifests -const TESTCASE_SCHEMA_LOCATION: &str = - "https://evidenceangel-schemas.hpkns.uk/testcase.1.schema.json"; -/// The schema itself for test case manifests (version 2) -pub(crate) const TESTCASE_SCHEMA: &str = include_str!("../../schemas/testcase.1.schema.json"); - -/// A test case stored within an [`EvidencePackage`](super::EvidencePackage). -#[derive(Clone, Debug, Serialize, Deserialize, Getters, MutGetters, Setters)] -pub struct TestCase { - /// The $schema from this test case - #[serde(rename = "$schema")] - schema: Option, - - /// The internal ID of this test case. - #[serde(skip)] - #[getset(get = "pub", set = "pub(super)")] - id: Uuid, - - /// The metadata of this test case. - #[getset(get = "pub", get_mut = "pub")] - metadata: TestCaseMetadata, - - /// The evidence in this test case. - #[getset(get = "pub", get_mut = "pub")] - evidence: Vec, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl TestCase { - /// Create a new test case - pub(super) fn new(id: Uuid, title: String, execution_datetime: DateTime) -> Self { - Self { - schema: Some(TESTCASE_SCHEMA_LOCATION.to_string()), - id, - metadata: TestCaseMetadata { - title, - execution_datetime, - passed: None, - custom: None, - extra_fields: HashMap::new(), - }, - evidence: vec![], - extra_fields: HashMap::new(), - } - } - - /// Update the JSON schema tag to the latest schema - pub(super) fn update_schema(&mut self) { - self.schema = Some(TESTCASE_SCHEMA_LOCATION.to_string()); - } -} - -/// The metadata of a [`TestCase`]. -#[derive(Clone, Debug, Serialize, Deserialize, Getters, Setters)] -#[getset(get = "pub", set = "pub")] -pub struct TestCaseMetadata { - /// The title of the associated [`TestCase`]. - title: String, - /// The time of execution of the associated [`TestCase`]. - execution_datetime: DateTime, - /// The state of the associated [`TestCase`]. - passed: Option, - /// Custom metadata parameters - #[serde(skip_serializing_if = "Option::is_none")] - custom: Option>, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl TestCaseMetadata { - /// Get a mutable reference to custom metadata parameters - #[allow(clippy::missing_panics_doc, reason = "safety is explained inline")] - pub fn custom_mut(&mut self) -> &mut HashMap { - if self.custom.is_none() { - self.custom = Some(HashMap::new()); - } - // SAFETY: just initialised if wasn't previously - self.custom.as_mut().unwrap() - } -} - -/// Valid test case statuses. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum TestCasePassStatus { - /// Passed - #[serde(rename = "pass")] - Pass, - /// Failed - #[serde(rename = "fail")] - Fail, -} - -/// Evidence in a [`TestCase`]. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, MutGetters, Setters)] -#[getset(get = "pub")] -pub struct Evidence { - /// The kind of this evidence. - kind: EvidenceKind, - - /// The data contained within this piece of evidence. - #[getset(get_mut = "pub", set = "pub")] - value: EvidenceData, - - /// A text caption associated with this piece of evidence. - #[getset(get_mut = "pub", set = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - caption: Option, - - /// The original filename, if this is `File` evidence. - /// This MUST be None for any other kind of evidence. - #[serde(skip_serializing_if = "Option::is_none")] - #[getset(get_mut = "pub", set = "pub")] - original_filename: Option, - - /// Extra fields that this implementation doesn't understand. - #[get = "pub"] - #[serde(flatten)] - extra_fields: HashMap, -} - -impl Evidence { - /// Create a new evidence object. - #[must_use] - pub fn new(kind: EvidenceKind, value: EvidenceData) -> Self { - Self { - kind, - value, - caption: None, - original_filename: None, - extra_fields: HashMap::new(), - } - } - - /// Get the data internal to this evidence as a byte array. - /// - /// # Panics - /// - /// In most cases, this can be seen as infallible. - /// - /// However, this will panic if the internal structure of the - /// evidence package is invalid, in this case if the data refers to - /// a media item that doesn't exist. - pub fn data(&self, pkg: &mut super::EvidencePackage) -> Vec { - match self.value() { - EvidenceData::Text { content } => content.as_bytes().to_vec(), - EvidenceData::Base64 { data } => data.clone(), - EvidenceData::Media { hash } => { - tracing::debug!("Fetching media with hash {hash}"); - let media = pkg.get_media(hash).ok().flatten(); - tracing::debug!("Got media {media:?}"); - media.unwrap().data().clone() - } - } - } - - /// Gets the associated media file media type, if one is present. - pub fn media_mime(&self, pkg: &mut super::EvidencePackage) -> Option { - match self.value() { - EvidenceData::Media { hash } => { - tracing::debug!("Fetching media with hash {hash}"); - pkg.media - .iter() - .find(|mfme| mfme.sha256_checksum() == hash) - .map(|mfme| mfme.mime_type().clone()) - } - _ => None, - } - } -} - -/// 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 { - /// Text based data. - Text { - /// The text-based content. - content: String, - }, - /// Base64 encoded data. - Base64 { - /// 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. - Media { - /// The hash of the [`MediaFile`](crate::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, - }, -} - -impl EvidenceData { - /// Get the data from this object. This will fetch the media file if needed. - /// - /// # 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> { - 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())), - } - } -} - -/// The serde visitor for parsing evidence data values (i.e. `plain:`, `base64:` -/// and `media:`) -struct EvidenceDataVisitor; - -impl Visitor<'_> for EvidenceDataVisitor { - type Value = EvidenceData; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str(r#"evidence data, in the format: "kind:value""#) - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - Self::visit_string(self, v.to_string()) - } - - fn visit_string(self, v: String) -> Result - where - E: serde::de::Error, - { - if let Some((typ, dat)) = v.split_once(':') { - return match typ { - "plain" => Ok(EvidenceData::Text { - content: dat.to_string(), - }), - "base64" => Ok(EvidenceData::Base64 { - data: base64::prelude::BASE64_STANDARD_NO_PAD - .decode(dat) - .map_err(serde::de::Error::custom)?, - }), - "media" => Ok(EvidenceData::Media { - hash: dat.to_string(), - }), - _ => Err(serde::de::Error::custom(format!( - "invalid type {typ}, expected one of plain, base64 or media" - ))), - }; - } - Err(de::Error::custom( - "invalid format, expected string with `:' separator", - )) - } -} - -impl Serialize for EvidenceData { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let data_s = match self { - Self::Text { content } => format!("plain:{content}"), - Self::Base64 { data } => format!( - "base64:{}", - base64::prelude::BASE64_STANDARD_NO_PAD.encode(data) - ), - Self::Media { hash } => format!("media:{hash}"), - }; - serializer.serialize_str(&data_s) - } -} - -impl<'de> Deserialize<'de> for EvidenceData { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_string(EvidenceDataVisitor) - } -} diff --git a/src/result.rs b/src/result.rs deleted file mode 100644 index d3df935..0000000 --- a/src/result.rs +++ /dev/null @@ -1,67 +0,0 @@ -use thiserror::Error; -use uuid::Uuid; - -/// An error raised by `EvidenceAngel`. -#[derive(Debug, Error)] -pub enum Error { - /// You are trying to perform an operation without a lock on the package. - #[error( - "The file you are working with is already open. Please close it and try again. If you are sure no one else if working with it, you can delete the lock file and try again." - )] - LockNotObtained, - - /// An I/O error from the system. - #[error("I/O Error: {0}")] - Io(#[from] std::io::Error), - - /// A package error, i.e. raised by the `zip` package. - #[error("Package error: {0}")] - Zip(#[from] zip::result::ZipError), - - /// The package is corrupt. See the contained string for more details. - #[error("The evidence package is corrupt ({0}).")] - CorruptEvidencePackage(String), - - /// The package manifest isn't valid JSON. - #[error("The package manifest is corrupt: {0}")] - InvalidManifest(serde_json::Error), - - /// The package couldn't be created due to a JSON error. - #[error("Failed to create package: {0}")] - FailedToCreatePackage(serde_json::Error), - - /// The test case couldn't be saved due to a JSON error. - #[error("Failed to save test case: {0}")] - FailedToSaveTestCase(serde_json::Error), - - /// The test case couldn't be read due to a JSON error. - #[error("Failed to read test case {1}: {0}")] - InvalidTestCase(serde_json::Error, Uuid), - - /// Some media is missing from the package. - #[error("Media is missing from the package with hash {0}")] - MediaMissing(String), - - /// Validation against the manifest schema failed. - #[error( - "Manifest schema validation failed. Perhaps this package is from a newer version of EvidenceAngel." - )] - ManifestSchemaValidationFailed, - - /// Validation against the test case schema failed. - #[error( - "Test case schema validation failed. Perhaps this package is from a newer version of EvidenceAngel." - )] - TestCaseSchemaValidationFailed, - - /// The specified test case doesn't exist - #[error("The specified test case doesn't exist")] - DoesntExist(Uuid), - - /// An otherwise unhandled error occured during export. - #[error("Export failed: {0}")] - OtherExportError(Box), -} - -/// A result from `EvidenceAngel`. -pub type Result = std::result::Result; diff --git a/src/zip_read_writer.rs b/src/zip_read_writer.rs deleted file mode 100644 index 8ac3097..0000000 --- a/src/zip_read_writer.rs +++ /dev/null @@ -1,210 +0,0 @@ -use std::{ - fmt, fs, - io::{BufReader, BufWriter}, - path, -}; - -use zip::{ZipArchive, ZipWriter}; - -use crate::lock_file::LockFile; - -/// A convenient type which can read and write to a ZIP file and cleanly switch between the two modes. -/// -/// Whilst writing, you can also read the previous file, as it writes to a new temporary file, until -/// [`ZipReaderWriter::conclude_write`] is called. -#[derive(Default)] -pub(crate) struct ZipReaderWriter { - /// The path of this zip file, if set - path: Option, - /// The locking file for this evidence package. If this is [`Some`], - /// you can be assured that the lock is obtained. - lock_file: Option, - /// The reader, if in write mode, of this reader/writer - reader: Option>>, - /// The writer, if in write mode, of this reader/writer - writer: Option>>, -} - -impl Clone for ZipReaderWriter { - fn clone(&self) -> Self { - Self { - path: self.path.clone(), - ..Default::default() - } - } -} - -impl fmt::Debug for ZipReaderWriter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mode = if self.reader.is_some() { - "read" - } else if self.writer.is_some() { - "write" - } else { - "idle" - }; - f.debug_struct("ZipReadWriter") - .field("file", &self.path) - .field("current_mode", &mode) - .finish_non_exhaustive() - } -} - -impl ZipReaderWriter { - /// Create a new [`ZipReaderWriter`] instance. - pub fn new(path: path::PathBuf) -> crate::Result { - let mut o = Self { - path: Some(path), - ..Default::default() - }; - o.update_lock_file()?; - Ok(o) - } - - /// Validate that the currently held lock is still locking the - /// package. - fn validate_lock(&mut self) -> crate::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 - }) - } else { - Err(crate::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<()> { - if let Some(path) = &self.path { - let mut lock_path = path.clone(); - // SAFETY: only a file can be specified here - lock_path.set_file_name(format!( - ".~lock.{}", - lock_path.file_name().unwrap().to_str().unwrap() - )); - // SAFETY: only a file can be specified here - lock_path.set_extension(format!( - "{}#", - lock_path.extension().unwrap().to_str().unwrap() - )); - self.lock_file = Some(LockFile::new(lock_path).map_err(|e| { - tracing::error!("Locking error: {e}"); - crate::Error::LockNotObtained - })?); - } else { - self.lock_file = None; - } - Ok(()) - } - - /// Get this [`ZipReaderWriter`] instance in read mode. - pub fn as_reader(&mut self) -> crate::Result<&mut ZipArchive>> { - if self.reader.is_none() { - // Close writer - tracing::debug!("Closing writer"); - self.writer = None; - - // Open reader - tracing::debug!("Opening reader"); - self.reader = Some(ZipArchive::new(BufReader::new(fs::File::open( - self.path - .as_ref() - .expect("zipreadwriter must not be called upon until file is set."), - )?))?); - } - Ok(self.reader.as_mut().unwrap()) - } - - /// Get this [`ZipReaderWriter`] instance in write mode. - #[allow(clippy::type_complexity)] - pub fn as_writer( - &mut self, - ) -> crate::Result<( - Option<&mut ZipArchive>>, - &mut ZipWriter>, - )> { - self.validate_lock()?; - if self.writer.is_none() { - tracing::debug!("Opening writer"); - // Open writer - self.writer = Some(ZipWriter::new(BufWriter::new(fs::File::create( - self.path - .as_ref() - .map(|p| { - let mut p = p.clone(); - p.set_file_name(format!( - "{}.tmp", - p.file_name().unwrap().to_string_lossy() - )); - p - }) - .expect("zipreadwriter must not be called upon until file is set."), - )?))); - } - Ok((self.reader.as_mut(), self.writer.as_mut().unwrap())) - } - - /// Conclude writing to the ZIP file and reset for reading or writing again. - pub fn conclude_write(&mut self) -> crate::Result<()> { - self.validate_lock()?; - if self.writer.is_some() { - // Close write - tracing::debug!("Closing writer"); - let writer = self.writer.take().unwrap(); - writer.finish()?; - - tracing::debug!("Closing reader"); - self.reader = None; - - // Move temp file - tracing::debug!("Moving temp file to overwrite package"); - let tmp_path = self - .path - .as_ref() - .map(|p| { - let mut p = p.clone(); - p.set_file_name(format!("{}.tmp", p.file_name().unwrap().to_string_lossy())); - p - }) - .expect("zipreadwriter must not be called upon until file is set."); - fs::rename( - tmp_path, - self.path - .as_ref() - .expect("zipreadwriter must not be called upon until file is set."), - )?; - } - Ok(()) - } - - /// Interrupt a write early, concluding the write and removing the temporary file. - pub fn interrupt_write(&mut self) -> crate::Result<()> { - self.validate_lock()?; - if self.writer.is_some() { - // Close write - tracing::debug!("Closing writer"); - let writer = self.writer.take().unwrap(); - writer.finish()?; - - tracing::debug!("Closing reader"); - self.reader = None; - - // Delete temp file - tracing::debug!("Moving temp file to overwrite package"); - let tmp_path = self - .path - .as_ref() - .map(|p| { - let mut p = p.clone(); - p.set_file_name(format!("{}.tmp", p.file_name().unwrap().to_string_lossy())); - p - }) - .expect("zipreadwriter must not be called upon until file is set."); - fs::remove_file(tmp_path)?; - } - Ok(()) - } -} diff --git a/ui/Cargo.toml b/ui/Cargo.toml new file mode 100644 index 0000000..26caeb3 --- /dev/null +++ b/ui/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "evidenceangel-ui" +version.workspace = true +license.workspace = true +edition.workspace = true +authors.workspace = true + +[features] +windows-keep-console-window = [] + +[dependencies] +chrono = "0.4.41" +clap = { version = "4.5.4", features = ["derive"] } +directories = "6.0.0" +evp = { version = "1.0.0-beta.1", features = ["exporter-excel", "exporter-html", "exporter-zip-of-files"] } +fluent-templates = "0.13.0" +getset = "0.1.6" +infer = "0.19.0" +open = "5.3.0" +parking_lot = "0.12.3" +parse_datetime = "0.10.0" +relm4 = { version = "0.9.0", features = [ + "libadwaita", + "gnome_46", +] } +relm4-icons = "0.9.0" +sys-locale = "0.3.1" +tempfile = "3.20.0" +tracing = "0.1.41" +tracing-panic = "0.1.2" +tracing-subscriber-multi = "0.1.0" +uuid = "1.18.0" + +[target.'cfg(windows)'.build-dependencies] +winresource = "0.1" +ico-builder = "0.1" + +[build-dependencies] +glib-build-tools = { version = "0.21.0" } +mdbook = { version = "0.4.48", default-features = false, features = ["search"] } diff --git a/ui/build.rs b/ui/build.rs new file mode 100644 index 0000000..40f687b --- /dev/null +++ b/ui/build.rs @@ -0,0 +1,39 @@ +fn main() { + // Build hicolor icons + println!("cargo::rerun-if-changed=resources"); + println!("cargo::rerun-if-changed=hicolor-icon.gresource.xml"); + glib_build_tools::compile_resources( + &["resources"], + "hicolor-icon.gresource.xml", + "hicolor-icon.gresource", + ); + + // Build documentation + println!("cargo::rerun-if-changed=docs/book.toml"); + println!("cargo::rerun-if-changed=docs/src"); + let docs_book = + mdbook::MDBook::load("docs").expect("Failed to load documentation for EvidenceAngel"); + docs_book + .build() + .expect("Failed to build documentation for EvidenceAngel"); + + // Build icon + println!("cargo::rerun-if-changed=icon.png"); + #[cfg(windows)] + { + ico_builder::IcoBuilder::default() + .add_source_file("icon.png") + .build_file("icon.ico") + .unwrap(); + + ico_builder::IcoBuilder::default() + .add_source_file("file_icon.png") + .build_file("file_icon.ico") + .unwrap(); + + let mut res = winresource::WindowsResource::new(); + res.set_icon("icon.ico"); + res.set_icon_with_id("file_icon.ico", "2"); + res.compile().unwrap(); + } +} diff --git a/docs/.gitignore b/ui/docs/.gitignore similarity index 100% rename from docs/.gitignore rename to ui/docs/.gitignore diff --git a/docs/book.toml b/ui/docs/book.toml similarity index 100% rename from docs/book.toml rename to ui/docs/book.toml diff --git a/docs/src/SUMMARY.md b/ui/docs/src/SUMMARY.md similarity index 100% rename from docs/src/SUMMARY.md rename to ui/docs/src/SUMMARY.md diff --git a/docs/src/cli.md b/ui/docs/src/cli.md similarity index 100% rename from docs/src/cli.md rename to ui/docs/src/cli.md diff --git a/docs/src/creating_a_package.md b/ui/docs/src/creating_a_package.md similarity index 100% rename from docs/src/creating_a_package.md rename to ui/docs/src/creating_a_package.md diff --git a/docs/src/creating_a_test_case.md b/ui/docs/src/creating_a_test_case.md similarity index 100% rename from docs/src/creating_a_test_case.md rename to ui/docs/src/creating_a_test_case.md diff --git a/docs/src/exporting.md b/ui/docs/src/exporting.md similarity index 100% rename from docs/src/exporting.md rename to ui/docs/src/exporting.md diff --git a/docs/src/getting_started.md b/ui/docs/src/getting_started.md similarity index 100% rename from docs/src/getting_started.md rename to ui/docs/src/getting_started.md diff --git a/docs/src/glossary.md b/ui/docs/src/glossary.md similarity index 100% rename from docs/src/glossary.md rename to ui/docs/src/glossary.md diff --git a/docs/src/images/creating_a_package/0_nothing_is_open.png b/ui/docs/src/images/creating_a_package/0_nothing_is_open.png similarity index 100% rename from docs/src/images/creating_a_package/0_nothing_is_open.png rename to ui/docs/src/images/creating_a_package/0_nothing_is_open.png diff --git a/docs/src/images/creating_a_package/1_menu_button.png b/ui/docs/src/images/creating_a_package/1_menu_button.png similarity index 100% rename from docs/src/images/creating_a_package/1_menu_button.png rename to ui/docs/src/images/creating_a_package/1_menu_button.png diff --git a/docs/src/images/creating_a_package/2_menu_new.png b/ui/docs/src/images/creating_a_package/2_menu_new.png similarity index 100% rename from docs/src/images/creating_a_package/2_menu_new.png rename to ui/docs/src/images/creating_a_package/2_menu_new.png diff --git a/docs/src/images/creating_a_package/3_no_case_open.png b/ui/docs/src/images/creating_a_package/3_no_case_open.png similarity index 100% rename from docs/src/images/creating_a_package/3_no_case_open.png rename to ui/docs/src/images/creating_a_package/3_no_case_open.png diff --git a/docs/src/images/creating_a_package/4_metadata.png b/ui/docs/src/images/creating_a_package/4_metadata.png similarity index 100% rename from docs/src/images/creating_a_package/4_metadata.png rename to ui/docs/src/images/creating_a_package/4_metadata.png diff --git a/docs/src/images/creating_a_package/5_package_metadata.png b/ui/docs/src/images/creating_a_package/5_package_metadata.png similarity index 100% rename from docs/src/images/creating_a_package/5_package_metadata.png rename to ui/docs/src/images/creating_a_package/5_package_metadata.png diff --git a/docs/src/images/creating_a_package/6_custom_metadata.png b/ui/docs/src/images/creating_a_package/6_custom_metadata.png similarity index 100% rename from docs/src/images/creating_a_package/6_custom_metadata.png rename to ui/docs/src/images/creating_a_package/6_custom_metadata.png diff --git a/docs/src/images/creating_a_test_case/0_create_test_case.png b/ui/docs/src/images/creating_a_test_case/0_create_test_case.png similarity index 100% rename from docs/src/images/creating_a_test_case/0_create_test_case.png rename to ui/docs/src/images/creating_a_test_case/0_create_test_case.png diff --git a/docs/src/images/creating_a_test_case/2_new_test_case.png b/ui/docs/src/images/creating_a_test_case/2_new_test_case.png similarity index 100% rename from docs/src/images/creating_a_test_case/2_new_test_case.png rename to ui/docs/src/images/creating_a_test_case/2_new_test_case.png diff --git a/docs/src/images/creating_a_test_case/3_test_case_edit.png b/ui/docs/src/images/creating_a_test_case/3_test_case_edit.png similarity index 100% rename from docs/src/images/creating_a_test_case/3_test_case_edit.png rename to ui/docs/src/images/creating_a_test_case/3_test_case_edit.png diff --git a/docs/src/images/creating_a_test_case/4_unsaved.png b/ui/docs/src/images/creating_a_test_case/4_unsaved.png similarity index 100% rename from docs/src/images/creating_a_test_case/4_unsaved.png rename to ui/docs/src/images/creating_a_test_case/4_unsaved.png diff --git a/docs/src/images/creating_a_test_case/5_test_case_actions.png b/ui/docs/src/images/creating_a_test_case/5_test_case_actions.png similarity index 100% rename from docs/src/images/creating_a_test_case/5_test_case_actions.png rename to ui/docs/src/images/creating_a_test_case/5_test_case_actions.png diff --git a/docs/src/images/exporting/0_menu_button.png b/ui/docs/src/images/exporting/0_menu_button.png similarity index 100% rename from docs/src/images/exporting/0_menu_button.png rename to ui/docs/src/images/exporting/0_menu_button.png diff --git a/docs/src/images/exporting/1_menu_export.png b/ui/docs/src/images/exporting/1_menu_export.png similarity index 100% rename from docs/src/images/exporting/1_menu_export.png rename to ui/docs/src/images/exporting/1_menu_export.png diff --git a/docs/src/images/exporting/2_select_format.png b/ui/docs/src/images/exporting/2_select_format.png similarity index 100% rename from docs/src/images/exporting/2_select_format.png rename to ui/docs/src/images/exporting/2_select_format.png diff --git a/docs/src/images/exporting/3_select_destination.png b/ui/docs/src/images/exporting/3_select_destination.png similarity index 100% rename from docs/src/images/exporting/3_select_destination.png rename to ui/docs/src/images/exporting/3_select_destination.png diff --git a/docs/src/images/rebuild_images.sh b/ui/docs/src/images/rebuild_images.sh similarity index 100% rename from docs/src/images/rebuild_images.sh rename to ui/docs/src/images/rebuild_images.sh diff --git a/docs/src/images/sources/export.png b/ui/docs/src/images/sources/export.png similarity index 100% rename from docs/src/images/sources/export.png rename to ui/docs/src/images/sources/export.png diff --git a/docs/src/images/sources/menu.png b/ui/docs/src/images/sources/menu.png similarity index 100% rename from docs/src/images/sources/menu.png rename to ui/docs/src/images/sources/menu.png diff --git a/docs/src/images/sources/new_package.png b/ui/docs/src/images/sources/new_package.png similarity index 100% rename from docs/src/images/sources/new_package.png rename to ui/docs/src/images/sources/new_package.png diff --git a/docs/src/images/sources/nothing_open.png b/ui/docs/src/images/sources/nothing_open.png similarity index 100% rename from docs/src/images/sources/nothing_open.png rename to ui/docs/src/images/sources/nothing_open.png diff --git a/docs/src/images/sources/overlays/add_evidence.png b/ui/docs/src/images/sources/overlays/add_evidence.png similarity index 100% rename from docs/src/images/sources/overlays/add_evidence.png rename to ui/docs/src/images/sources/overlays/add_evidence.png diff --git a/docs/src/images/sources/overlays/add_evidence.svg b/ui/docs/src/images/sources/overlays/add_evidence.svg similarity index 100% rename from docs/src/images/sources/overlays/add_evidence.svg rename to ui/docs/src/images/sources/overlays/add_evidence.svg diff --git a/docs/src/images/sources/overlays/create_case_button.png b/ui/docs/src/images/sources/overlays/create_case_button.png similarity index 100% rename from docs/src/images/sources/overlays/create_case_button.png rename to ui/docs/src/images/sources/overlays/create_case_button.png diff --git a/docs/src/images/sources/overlays/create_case_button.svg b/ui/docs/src/images/sources/overlays/create_case_button.svg similarity index 100% rename from docs/src/images/sources/overlays/create_case_button.svg rename to ui/docs/src/images/sources/overlays/create_case_button.svg diff --git a/docs/src/images/sources/overlays/export_format.png b/ui/docs/src/images/sources/overlays/export_format.png similarity index 100% rename from docs/src/images/sources/overlays/export_format.png rename to ui/docs/src/images/sources/overlays/export_format.png diff --git a/docs/src/images/sources/overlays/export_format.svg b/ui/docs/src/images/sources/overlays/export_format.svg similarity index 100% rename from docs/src/images/sources/overlays/export_format.svg rename to ui/docs/src/images/sources/overlays/export_format.svg diff --git a/docs/src/images/sources/overlays/export_target.png b/ui/docs/src/images/sources/overlays/export_target.png similarity index 100% rename from docs/src/images/sources/overlays/export_target.png rename to ui/docs/src/images/sources/overlays/export_target.png diff --git a/docs/src/images/sources/overlays/export_target.svg b/ui/docs/src/images/sources/overlays/export_target.svg similarity index 100% rename from docs/src/images/sources/overlays/export_target.svg rename to ui/docs/src/images/sources/overlays/export_target.svg diff --git a/docs/src/images/sources/overlays/menu_button.png b/ui/docs/src/images/sources/overlays/menu_button.png similarity index 100% rename from docs/src/images/sources/overlays/menu_button.png rename to ui/docs/src/images/sources/overlays/menu_button.png diff --git a/docs/src/images/sources/overlays/menu_button.svg b/ui/docs/src/images/sources/overlays/menu_button.svg similarity index 100% rename from docs/src/images/sources/overlays/menu_button.svg rename to ui/docs/src/images/sources/overlays/menu_button.svg diff --git a/docs/src/images/sources/overlays/menu_export.png b/ui/docs/src/images/sources/overlays/menu_export.png similarity index 100% rename from docs/src/images/sources/overlays/menu_export.png rename to ui/docs/src/images/sources/overlays/menu_export.png diff --git a/docs/src/images/sources/overlays/menu_export.svg b/ui/docs/src/images/sources/overlays/menu_export.svg similarity index 100% rename from docs/src/images/sources/overlays/menu_export.svg rename to ui/docs/src/images/sources/overlays/menu_export.svg diff --git a/docs/src/images/sources/overlays/menu_new.png b/ui/docs/src/images/sources/overlays/menu_new.png similarity index 100% rename from docs/src/images/sources/overlays/menu_new.png rename to ui/docs/src/images/sources/overlays/menu_new.png diff --git a/docs/src/images/sources/overlays/menu_new.svg b/ui/docs/src/images/sources/overlays/menu_new.svg similarity index 100% rename from docs/src/images/sources/overlays/menu_new.svg rename to ui/docs/src/images/sources/overlays/menu_new.svg diff --git a/docs/src/images/sources/overlays/menu_paste_evidence.png b/ui/docs/src/images/sources/overlays/menu_paste_evidence.png similarity index 100% rename from docs/src/images/sources/overlays/menu_paste_evidence.png rename to ui/docs/src/images/sources/overlays/menu_paste_evidence.png diff --git a/docs/src/images/sources/overlays/menu_paste_evidence.svg b/ui/docs/src/images/sources/overlays/menu_paste_evidence.svg similarity index 100% rename from docs/src/images/sources/overlays/menu_paste_evidence.svg rename to ui/docs/src/images/sources/overlays/menu_paste_evidence.svg diff --git a/docs/src/images/sources/overlays/nav_metadata.png b/ui/docs/src/images/sources/overlays/nav_metadata.png similarity index 100% rename from docs/src/images/sources/overlays/nav_metadata.png rename to ui/docs/src/images/sources/overlays/nav_metadata.png diff --git a/docs/src/images/sources/overlays/nav_metadata.svg b/ui/docs/src/images/sources/overlays/nav_metadata.svg similarity index 100% rename from docs/src/images/sources/overlays/nav_metadata.svg rename to ui/docs/src/images/sources/overlays/nav_metadata.svg diff --git a/docs/src/images/sources/overlays/package_metadata.png b/ui/docs/src/images/sources/overlays/package_metadata.png similarity index 100% rename from docs/src/images/sources/overlays/package_metadata.png rename to ui/docs/src/images/sources/overlays/package_metadata.png diff --git a/docs/src/images/sources/overlays/package_metadata.svg b/ui/docs/src/images/sources/overlays/package_metadata.svg similarity index 100% rename from docs/src/images/sources/overlays/package_metadata.svg rename to ui/docs/src/images/sources/overlays/package_metadata.svg diff --git a/docs/src/images/sources/overlays/package_metadata_custom_fields.png b/ui/docs/src/images/sources/overlays/package_metadata_custom_fields.png similarity index 100% rename from docs/src/images/sources/overlays/package_metadata_custom_fields.png rename to ui/docs/src/images/sources/overlays/package_metadata_custom_fields.png diff --git a/docs/src/images/sources/overlays/package_metadata_custom_fields.svg b/ui/docs/src/images/sources/overlays/package_metadata_custom_fields.svg similarity index 100% rename from docs/src/images/sources/overlays/package_metadata_custom_fields.svg rename to ui/docs/src/images/sources/overlays/package_metadata_custom_fields.svg diff --git a/docs/src/images/sources/overlays/test_case_actions.png b/ui/docs/src/images/sources/overlays/test_case_actions.png similarity index 100% rename from docs/src/images/sources/overlays/test_case_actions.png rename to ui/docs/src/images/sources/overlays/test_case_actions.png diff --git a/docs/src/images/sources/overlays/test_case_actions.svg b/ui/docs/src/images/sources/overlays/test_case_actions.svg similarity index 100% rename from docs/src/images/sources/overlays/test_case_actions.svg rename to ui/docs/src/images/sources/overlays/test_case_actions.svg diff --git a/docs/src/images/sources/overlays/test_case_metadata.png b/ui/docs/src/images/sources/overlays/test_case_metadata.png similarity index 100% rename from docs/src/images/sources/overlays/test_case_metadata.png rename to ui/docs/src/images/sources/overlays/test_case_metadata.png diff --git a/docs/src/images/sources/overlays/test_case_metadata.svg b/ui/docs/src/images/sources/overlays/test_case_metadata.svg similarity index 100% rename from docs/src/images/sources/overlays/test_case_metadata.svg rename to ui/docs/src/images/sources/overlays/test_case_metadata.svg diff --git a/docs/src/images/sources/package_metadata.png b/ui/docs/src/images/sources/package_metadata.png similarity index 100% rename from docs/src/images/sources/package_metadata.png rename to ui/docs/src/images/sources/package_metadata.png diff --git a/docs/src/images/sources/test_case.png b/ui/docs/src/images/sources/test_case.png similarity index 100% rename from docs/src/images/sources/test_case.png rename to ui/docs/src/images/sources/test_case.png diff --git a/docs/src/images/sources/unsaved.png b/ui/docs/src/images/sources/unsaved.png similarity index 100% rename from docs/src/images/sources/unsaved.png rename to ui/docs/src/images/sources/unsaved.png diff --git a/docs/src/images/taking_evidence/0_menu_button.png b/ui/docs/src/images/taking_evidence/0_menu_button.png similarity index 100% rename from docs/src/images/taking_evidence/0_menu_button.png rename to ui/docs/src/images/taking_evidence/0_menu_button.png diff --git a/docs/src/images/taking_evidence/1_menu_paste_evidence.png b/ui/docs/src/images/taking_evidence/1_menu_paste_evidence.png similarity index 100% rename from docs/src/images/taking_evidence/1_menu_paste_evidence.png rename to ui/docs/src/images/taking_evidence/1_menu_paste_evidence.png diff --git a/docs/src/images/taking_evidence/2_add_evidence.png b/ui/docs/src/images/taking_evidence/2_add_evidence.png similarity index 100% rename from docs/src/images/taking_evidence/2_add_evidence.png rename to ui/docs/src/images/taking_evidence/2_add_evidence.png diff --git a/docs/src/taking_evidence.md b/ui/docs/src/taking_evidence.md similarity index 100% rename from docs/src/taking_evidence.md rename to ui/docs/src/taking_evidence.md diff --git a/file_icon.png b/ui/file_icon.png similarity index 100% rename from file_icon.png rename to ui/file_icon.png diff --git a/hicolor-icon.gresource.xml b/ui/hicolor-icon.gresource.xml similarity index 100% rename from hicolor-icon.gresource.xml rename to ui/hicolor-icon.gresource.xml diff --git a/icon.png b/ui/icon.png similarity index 100% rename from icon.png rename to ui/icon.png diff --git a/resources/evidenceangel.svg b/ui/resources/evidenceangel.svg similarity index 100% rename from resources/evidenceangel.svg rename to ui/resources/evidenceangel.svg diff --git a/src/evidenceangel-ui/about.rs b/ui/src/about.rs similarity index 100% rename from src/evidenceangel-ui/about.rs rename to ui/src/about.rs diff --git a/src/evidenceangel-ui/app.rs b/ui/src/app.rs similarity index 99% rename from src/evidenceangel-ui/app.rs rename to ui/src/app.rs index be3275a..309cd0f 100644 --- a/src/evidenceangel-ui/app.rs +++ b/ui/src/app.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, sync::Arc}; use adw::prelude::*; -use evidenceangel::{ +use evp::{ Author, Evidence, EvidenceData, EvidenceKind, EvidencePackage, MediaFile, TestCasePassStatus, exporters::{ Exporter, excel::ExcelExporter, html::HtmlExporter, zip_of_files::ZipOfFilesExporter, @@ -93,7 +93,7 @@ pub struct AppModel { } impl AppModel { - fn open_new(&mut self) -> evidenceangel::Result<()> { + fn open_new(&mut self) -> evp::Result<()> { let title = lang::lookup("default-title"); let authors = vec![Author::new(lang::lookup("default-author"))]; let pkg = EvidencePackage::new( @@ -114,7 +114,7 @@ impl AppModel { Ok(()) } - fn open(&mut self, path: PathBuf) -> evidenceangel::Result<()> { + fn open(&mut self, path: PathBuf) -> evp::Result<()> { let pkg = EvidencePackage::open(path.clone())?; tracing::debug!("Package opened: {pkg:?}"); self.open_package = Some(Arc::new(RwLock::new(pkg))); @@ -138,7 +138,7 @@ impl AppModel { } /// Update nav menu with test cases from the currently open package. - fn update_nav_menu(&mut self) -> evidenceangel::Result<()> { + fn update_nav_menu(&mut self) -> evp::Result<()> { let mut test_case_data = self.test_case_nav_factory.guard(); test_case_data.clear(); if let Some(pkg) = self.open_package.as_ref() { @@ -1101,7 +1101,7 @@ impl Component for AppModel { )), }) .forward(sender.input_sender(), |msg| match msg {}); - if matches!(e, evidenceangel::Error::LockNotObtained) { + if matches!(e, evp::Error::LockNotObtained) { // also offer to release lock let lock_file_name = format!(".~lock.{}#", path.file_name().unwrap().to_str().unwrap()); @@ -2332,7 +2332,7 @@ impl Component for AppModel { if let Some(data) = cb.ok().flatten() { let evidence = Evidence::new( EvidenceKind::Text, - evidenceangel::EvidenceData::Text { + evp::EvidenceData::Text { content: data.to_string(), }, ); @@ -2359,7 +2359,7 @@ impl Component for AppModel { ); let evidence = Evidence::new( EvidenceKind::Image, - evidenceangel::EvidenceData::Media { + evp::EvidenceData::Media { hash: media.hash(), }, ); diff --git a/src/evidenceangel-ui/author_factory.rs b/ui/src/author_factory.rs similarity index 98% rename from src/evidenceangel-ui/author_factory.rs rename to ui/src/author_factory.rs index 08fc023..34ee8aa 100644 --- a/src/evidenceangel-ui/author_factory.rs +++ b/ui/src/author_factory.rs @@ -1,5 +1,5 @@ use adw::prelude::*; -use evidenceangel::Author; +use evp::Author; use relm4::{ FactorySender, RelmWidgetExt, adw, factory::FactoryView, diff --git a/src/evidenceangel-ui/custom_metadata_editor_factory.rs b/ui/src/custom_metadata_editor_factory.rs similarity index 99% rename from src/evidenceangel-ui/custom_metadata_editor_factory.rs rename to ui/src/custom_metadata_editor_factory.rs index 2eea45b..7ae4b4e 100644 --- a/src/evidenceangel-ui/custom_metadata_editor_factory.rs +++ b/ui/src/custom_metadata_editor_factory.rs @@ -1,5 +1,5 @@ use adw::prelude::*; -use evidenceangel::CustomMetadataField; +use evp::CustomMetadataField; use relm4::{ Component, ComponentController, Controller, FactorySender, RelmWidgetExt, adw, factory::FactoryView, diff --git a/src/evidenceangel-ui/custom_metadata_factory.rs b/ui/src/custom_metadata_factory.rs similarity index 98% rename from src/evidenceangel-ui/custom_metadata_factory.rs rename to ui/src/custom_metadata_factory.rs index 8c7c888..551c458 100644 --- a/src/evidenceangel-ui/custom_metadata_factory.rs +++ b/ui/src/custom_metadata_factory.rs @@ -1,5 +1,5 @@ use adw::prelude::*; -use evidenceangel::CustomMetadataField; +use evp::CustomMetadataField; use relm4::{ FactorySender, adw, factory::FactoryView, diff --git a/src/evidenceangel-ui/dialogs/add_evidence.rs b/ui/src/dialogs/add_evidence.rs similarity index 99% rename from src/evidenceangel-ui/dialogs/add_evidence.rs rename to ui/src/dialogs/add_evidence.rs index c58b061..39dfa15 100644 --- a/src/evidenceangel-ui/dialogs/add_evidence.rs +++ b/ui/src/dialogs/add_evidence.rs @@ -5,7 +5,7 @@ use std::{ }; use adw::prelude::*; -use evidenceangel::{Evidence, EvidenceData, EvidenceKind, EvidencePackage, MediaFile}; +use evp::{Evidence, EvidenceData, EvidenceKind, EvidencePackage, MediaFile}; use parking_lot::RwLock; use relm4::{ Component, ComponentParts, ComponentSender, RelmWidgetExt, diff --git a/src/evidenceangel-ui/dialogs/custom_metadata_field.rs b/ui/src/dialogs/custom_metadata_field.rs similarity index 100% rename from src/evidenceangel-ui/dialogs/custom_metadata_field.rs rename to ui/src/dialogs/custom_metadata_field.rs diff --git a/src/evidenceangel-ui/dialogs/error.rs b/ui/src/dialogs/error.rs similarity index 87% rename from src/evidenceangel-ui/dialogs/error.rs rename to ui/src/dialogs/error.rs index d06896e..51506d4 100644 --- a/src/evidenceangel-ui/dialogs/error.rs +++ b/ui/src/dialogs/error.rs @@ -72,13 +72,14 @@ impl Component for ErrorDialogModel { ) { match message { ErrorDialogInput::Present(window) => { - let lock_file = self.lock_file.clone().unwrap(); + let lock_file = self.lock_file.clone(); root.clone() .choose(&window, None::<&Cancellable>, move |response| { if response == "unlock" { - // SAFETY: unlock isn't an option without a lock file - if let Err(e) = fs::remove_file(lock_file) { - tracing::warn!("Failed to remove lock file: {e}"); + if let Some(lock_file) = lock_file { + if let Err(e) = fs::remove_file(lock_file) { + tracing::warn!("Failed to remove lock file: {e}"); + } } } }); diff --git a/src/evidenceangel-ui/dialogs/export.rs b/ui/src/dialogs/export.rs similarity index 100% rename from src/evidenceangel-ui/dialogs/export.rs rename to ui/src/dialogs/export.rs diff --git a/src/evidenceangel-ui/dialogs/mod.rs b/ui/src/dialogs/mod.rs similarity index 100% rename from src/evidenceangel-ui/dialogs/mod.rs rename to ui/src/dialogs/mod.rs diff --git a/src/evidenceangel-ui/dialogs/new_author.rs b/ui/src/dialogs/new_author.rs similarity index 99% rename from src/evidenceangel-ui/dialogs/new_author.rs rename to ui/src/dialogs/new_author.rs index 5003e7d..8cd3bb6 100644 --- a/src/evidenceangel-ui/dialogs/new_author.rs +++ b/ui/src/dialogs/new_author.rs @@ -1,5 +1,5 @@ use adw::prelude::*; -use evidenceangel::Author; +use evp::Author; use relm4::{ Component, ComponentParts, ComponentSender, RelmWidgetExt, adw::{self, ApplicationWindow}, diff --git a/src/evidenceangel-ui/evidence_factory/file.rs b/ui/src/evidence_factory/file.rs similarity index 98% rename from src/evidenceangel-ui/evidence_factory/file.rs rename to ui/src/evidence_factory/file.rs index d260449..997b671 100644 --- a/src/evidenceangel-ui/evidence_factory/file.rs +++ b/ui/src/evidence_factory/file.rs @@ -1,6 +1,6 @@ use std::{fs, sync::Arc}; -use evidenceangel::{Evidence, EvidencePackage}; +use evp::{Evidence, EvidencePackage}; use gtk::prelude::*; use parking_lot::RwLock; use relm4::{ diff --git a/src/evidenceangel-ui/evidence_factory/http.rs b/ui/src/evidence_factory/http.rs similarity index 100% rename from src/evidenceangel-ui/evidence_factory/http.rs rename to ui/src/evidence_factory/http.rs diff --git a/src/evidenceangel-ui/evidence_factory/image.rs b/ui/src/evidence_factory/image.rs similarity index 98% rename from src/evidenceangel-ui/evidence_factory/image.rs rename to ui/src/evidence_factory/image.rs index 1c8da0a..b9de655 100644 --- a/src/evidenceangel-ui/evidence_factory/image.rs +++ b/ui/src/evidence_factory/image.rs @@ -1,6 +1,6 @@ use std::{fs, sync::Arc}; -use evidenceangel::{Evidence, EvidencePackage}; +use evp::{Evidence, EvidencePackage}; use gtk::prelude::*; use parking_lot::RwLock; use relm4::{Component, ComponentParts, ComponentSender, gtk}; diff --git a/src/evidenceangel-ui/evidence_factory/mod.rs b/ui/src/evidence_factory/mod.rs similarity index 99% rename from src/evidenceangel-ui/evidence_factory/mod.rs rename to ui/src/evidence_factory/mod.rs index 202f95a..d0634eb 100644 --- a/src/evidenceangel-ui/evidence_factory/mod.rs +++ b/ui/src/evidence_factory/mod.rs @@ -1,7 +1,7 @@ use std::{any::Any, sync::Arc}; use adw::prelude::*; -use evidenceangel::{Evidence, EvidenceData, EvidenceKind, EvidencePackage}; +use evp::{Evidence, EvidenceData, EvidenceKind, EvidencePackage}; #[allow(unused_imports)] use gtk::prelude::*; use parking_lot::RwLock; diff --git a/src/evidenceangel-ui/evidence_factory/rich_text.rs b/ui/src/evidence_factory/rich_text.rs similarity index 99% rename from src/evidenceangel-ui/evidence_factory/rich_text.rs rename to ui/src/evidence_factory/rich_text.rs index 21a6e78..a96eb16 100644 --- a/src/evidenceangel-ui/evidence_factory/rich_text.rs +++ b/ui/src/evidence_factory/rich_text.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use angelmark::{ +use evp::angelmark::{ AngelmarkLine, AngelmarkTable, AngelmarkTableRow, AngelmarkText, OwnedSpan, parse_angelmark, }; use gtk::prelude::*; diff --git a/src/evidenceangel-ui/evidence_factory/text.rs b/ui/src/evidence_factory/text.rs similarity index 100% rename from src/evidenceangel-ui/evidence_factory/text.rs rename to ui/src/evidence_factory/text.rs diff --git a/src/evidenceangel-ui/filter.rs b/ui/src/filter.rs similarity index 100% rename from src/evidenceangel-ui/filter.rs rename to ui/src/filter.rs diff --git a/src/evidenceangel-ui/lang.rs b/ui/src/lang.rs similarity index 98% rename from src/evidenceangel-ui/lang.rs rename to ui/src/lang.rs index bd949ee..80ee97e 100644 --- a/src/evidenceangel-ui/lang.rs +++ b/ui/src/lang.rs @@ -5,7 +5,7 @@ use parking_lot::Mutex; fluent_templates::static_loader! { static LOCALES = { - locales: "src/evidenceangel-ui/locales", + locales: "src/locales", fallback_language: "en", }; } diff --git a/src/evidenceangel-ui/locales/en/main.ftl b/ui/src/locales/en/main.ftl similarity index 100% rename from src/evidenceangel-ui/locales/en/main.ftl rename to ui/src/locales/en/main.ftl diff --git a/src/evidenceangel-ui/locales/sv/main.ftl b/ui/src/locales/sv/main.ftl similarity index 100% rename from src/evidenceangel-ui/locales/sv/main.ftl rename to ui/src/locales/sv/main.ftl diff --git a/src/evidenceangel-ui/main.rs b/ui/src/main.rs similarity index 100% rename from src/evidenceangel-ui/main.rs rename to ui/src/main.rs diff --git a/src/evidenceangel-ui/nav_factory.rs b/ui/src/nav_factory.rs similarity index 99% rename from src/evidenceangel-ui/nav_factory.rs rename to ui/src/nav_factory.rs index c50e01b..9b652b7 100644 --- a/src/evidenceangel-ui/nav_factory.rs +++ b/ui/src/nav_factory.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use evidenceangel::TestCasePassStatus; +use evp::TestCasePassStatus; use gtk::prelude::*; use relm4::{ FactorySender, diff --git a/src/evidenceangel-ui/util.rs b/ui/src/util.rs similarity index 96% rename from src/evidenceangel-ui/util.rs rename to ui/src/util.rs index ff76df5..12aeaea 100644 --- a/src/evidenceangel-ui/util.rs +++ b/ui/src/util.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use evidenceangel::Evidence; +use evp::Evidence; use getset::Getters; use relm4::gtk::glib; use uuid::Uuid; diff --git a/wix/.gitignore b/ui/wix/.gitignore similarity index 100% rename from wix/.gitignore rename to ui/wix/.gitignore diff --git a/wix/banner.bmp b/ui/wix/banner.bmp similarity index 100% rename from wix/banner.bmp rename to ui/wix/banner.bmp diff --git a/wix/generate_wix_script.sh b/ui/wix/generate_wix_script.sh similarity index 100% rename from wix/generate_wix_script.sh rename to ui/wix/generate_wix_script.sh diff --git a/wix/license.rtf b/ui/wix/license.rtf similarity index 100% rename from wix/license.rtf rename to ui/wix/license.rtf diff --git a/wix/main.wxs.in b/ui/wix/main.wxs.in similarity index 100% rename from wix/main.wxs.in rename to ui/wix/main.wxs.in From 067b7cbd983a50725dc167957f9a2c4b3f691b3f Mon Sep 17 00:00:00 2001 From: Lily Hopkins Date: Thu, 21 Aug 2025 10:13:08 +0100 Subject: [PATCH 2/4] ci: adjusted jobs --- .github/workflows/build.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2ccbbbb..5ff83fb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,7 +9,8 @@ on: env: EXECUTABLE_NAME: evidenceangel-ui CARGO_KEEP_CONSOLE_ON_WINDOWS_FEATURE: windows-keep-console-window - CARGO_EXTRA_BUILD_PARAMS: --features ui --bin evidenceangel-ui + # Build params for the UI + CARGO_EXTRA_BUILD_PARAMS: -p ui jobs: create-release: @@ -80,7 +81,7 @@ jobs: command: "build" target: ${{ matrix.platform.target }} toolchain: ${{ matrix.toolchain }} - args: "--features cli --bin evidenceangel-cli --locked --release" + args: "-p cli --locked --release" strip: true - name: Rename binary (linux and macos) run: mv target/${{ matrix.platform.target }}/release/evidenceangel-cli target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} @@ -88,8 +89,6 @@ jobs: - name: Rename binary (windows) run: mv target/${{ matrix.platform.target }}/release/evidenceangel-cli.exe target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} if: matrix.platform.os_name == 'Windows-x86_64' - - name: Generate SHA-256 - run: shasum -a 256 target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} | cut -d ' ' -f 1 > target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }}.sha256 - name: Release binary and SHA-256 checksum to GitHub uses: softprops/action-gh-release@v1 with: @@ -97,7 +96,6 @@ jobs: prerelease: ${{ needs.create-release.outputs.CARGO_PKG_PRERELEASE }} files: | target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} - target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }}.sha256 build-gtk: name: Build and Publish (${{ matrix.platform.os_name }}) @@ -211,12 +209,12 @@ jobs: cp bundle/bin/${{ env.EXECUTABLE_NAME }}-${{ matrix.platform.file_suffix }}.exe target/release/evidenceangel-ui.exe cp bundle/bin/${{ env.EXECUTABLE_NAME }}-console-${{ matrix.platform.file_suffix }}.exe target/release/evidenceangel-ui-console.exe echo Building CLI executable... - cargo build --release -F cli --bin evidenceangel-cli + cargo build --release -p cli echo Generating Wix script... rm -rf ${{ steps.wingtk-install.outputs.BASE_DIR }}/include rm -rf ${{ steps.wingtk-install.outputs.BASE_DIR }}/bin/*.exe - pushd wix || exit + pushd ui/wix || exit GTK4_PATH="${{ steps.wingtk-install.outputs.BASE_DIR }}" ./generate_wix_script.sh sed -i 's|/d/|D:/|g' main.wxs popd || exit From acaf5b55237f0adc35d9b8f3ec967917efdd34df Mon Sep 17 00:00:00 2001 From: Lily Hopkins Date: Thu, 21 Aug 2025 10:13:26 +0100 Subject: [PATCH 3/4] chore: bumped version --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b24f68..3248980 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,7 +776,7 @@ dependencies = [ [[package]] name = "evidenceangel-cli" -version = "1.6.0-alpha.2" +version = "1.6.0-alpha.3" dependencies = [ "chrono", "clap", @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "evidenceangel-ui" -version = "1.6.0-alpha.2" +version = "1.6.0-alpha.3" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index a43d661..ab1587d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "1.6.0-alpha.2" +version = "1.6.0-alpha.3" license = "GPL-3.0-or-later" edition = "2024" authors = [ From 3b33ace9334763542cc7ca92926480eccf4ebe87 Mon Sep 17 00:00:00 2001 From: Lily Hopkins Date: Thu, 21 Aug 2025 10:15:19 +0100 Subject: [PATCH 4/4] style: reformatted --- cli/src/angelmark.rs | 2 +- cli/src/test_cases.rs | 6 ++---- ui/src/app.rs | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cli/src/angelmark.rs b/cli/src/angelmark.rs index 77548a9..3e26aaa 100644 --- a/cli/src/angelmark.rs +++ b/cli/src/angelmark.rs @@ -1,5 +1,5 @@ -use evp::angelmark::AngelmarkText; use colored::Colorize; +use evp::angelmark::AngelmarkText; /// Convert [`AngelmarkText`] to a string with ANSI symbols for terminal display. pub(crate) fn angelmark_to_term(angelmark: &AngelmarkText) -> String { diff --git a/cli/src/test_cases.rs b/cli/src/test_cases.rs index cacfa15..2242ed3 100644 --- a/cli/src/test_cases.rs +++ b/cli/src/test_cases.rs @@ -6,13 +6,11 @@ use std::{ rc::Rc, }; -use evp::angelmark::{AngelmarkLine, AngelmarkTableAlignment, parse_angelmark}; use chrono::FixedOffset; use clap::{Subcommand, ValueEnum}; use colored::Colorize; -use evp::{ - Evidence, EvidenceData, EvidenceKind, EvidencePackage, MediaFile, TestCasePassStatus, -}; +use evp::angelmark::{AngelmarkLine, AngelmarkTableAlignment, parse_angelmark}; +use evp::{Evidence, EvidenceData, EvidenceKind, EvidencePackage, MediaFile, TestCasePassStatus}; use schemars::JsonSchema; use serde::Serialize; use uuid::Uuid; diff --git a/ui/src/app.rs b/ui/src/app.rs index 309cd0f..164827d 100644 --- a/ui/src/app.rs +++ b/ui/src/app.rs @@ -2359,9 +2359,7 @@ impl Component for AppModel { ); let evidence = Evidence::new( EvidenceKind::Image, - evp::EvidenceData::Media { - hash: media.hash(), - }, + evp::EvidenceData::Media { hash: media.hash() }, ); sender_c.input(AppInput::_AddMedia(media)); sender_c