Compare commits

...

286 Commits

Author SHA1 Message Date
Vincent Breitmoser
8fe1810cea just: remove translate-templates 2025-09-30 23:28:45 +02:00
Vincent Breitmoser
355b301c67 about: add to readme and adapt example routes file 2025-09-29 22:37:55 +02:00
Vincent Breitmoser
de2ab333a5 web: remove about pages from hagrid 2025-09-29 22:37:55 +02:00
Vincent Breitmoser
f272c7147e nix: add build support for zola-based about pages 2025-09-29 22:37:55 +02:00
Vincent Breitmoser
418c686398 about: adapt urls for base path 2025-09-29 22:37:55 +02:00
Vincent Breitmoser
a7eaa7ffe2 about: move about pages content to root, use /about as base uri 2025-09-29 22:37:55 +02:00
Vincent Breitmoser
8fe0204af2 about: remove load average from stats page 2025-09-29 22:37:55 +02:00
Vincent Breitmoser
820afd2a55 about: move into own directory 2025-09-29 22:37:55 +02:00
Nikita Karamov
ecfadc9b69 Add static files to Zola site 2025-09-29 22:37:55 +02:00
Nikita Karamov
fce27b590f Port "Stats" 2025-09-29 22:37:55 +02:00
Nikita Karamov
bc9c7b463f Port "API" 2025-09-29 22:37:55 +02:00
Nikita Karamov
f802a5d7b5 Port "News" and individual posts 2025-09-29 22:37:55 +02:00
Nikita Karamov
52b784aa22 Port "Usage" 2025-09-29 22:37:55 +02:00
Nikita Karamov
c412ee608e Port "FAQ" 2025-09-29 22:37:55 +02:00
Nikita Karamov
aec69b50af Port "Privacy" 2025-09-29 22:37:55 +02:00
Nikita Karamov
eb6591fc04 Port "About" 2025-09-29 22:37:55 +02:00
Nikita Karamov
a44fbbed5e Add Zola templates 2025-09-29 22:37:55 +02:00
Nikita Karamov
34b9b2733a Initialize a Zola site 2025-09-29 22:37:55 +02:00
Vincent Breitmoser
952fc3d6f2 Back out "Fix linting errors after upgrade to 1.90.0."
This backs out commit df19ececc3.
2025-09-29 22:29:00 +02:00
Vincent Breitmoser
893442bc4e ignore any warnings or lints in dump.rs 2025-09-29 21:45:42 +02:00
Vincent Breitmoser
178dfb9dec db: drop fs database code 2025-09-29 21:44:18 +02:00
Vincent Breitmoser
2e9a14f58e Back out "Fix linting errors after Rust version upgrade."
This backs out commit 0fe99ba962.
2025-09-29 21:17:35 +02:00
Vincent Breitmoser
2395244b8f Back out "Upgrade Rust toolchain: 1.86 -> 1.89"
This backs out commit 3285f19d09.
2025-09-29 21:17:35 +02:00
Vincent Breitmoser
8d21fde2c9 Back out "Upgrade Rust toolchain: 1.89 -> 1.90"
This backs out commit 9e0409bbac.
2025-09-29 21:17:35 +02:00
Zeke Fast
df19ececc3 Fix linting errors after upgrade to 1.90.0.
warning: manual implementation of `.is_multiple_of()`
  --> hagridctl/src/import.rs:95:12
   |
95 |         if (self.count_total % 10) != 0 {
   |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `!self.count_total.is_multiple_of(10)`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of
   = note: `#[warn(clippy::manual_is_multiple_of)]` on by default

warning: `hagridctl` (bin "hagridctl" test) generated 1 warning (run `cargo clippy --fix --bin "hagridctl" --tests` to apply 1 suggestion)
2025-09-28 10:50:46 +02:00
Zeke Fast
9e0409bbac Upgrade Rust toolchain: 1.89 -> 1.90
If you don't have toolchain installed and you use rustup run:

    $ rustup toolchain install --profile default --component rustfmt,clippy 1.90

NOTE: It might be that you have 1.90.0 installed as stable toolchain, in
that case you still have to run the above command to install exactly 1.90.

Command: `just upgrade-rust`

Changes:
- Upgrade version of used toolchain in the following places:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
2025-09-28 10:50:46 +02:00
Zeke Fast
deefbfabe6 Introduce "upgrade-rust" "just" recipe to automate Rust toolchain upgrade.
Functionality:

User faced functionality is available through `just upgrade-rust`
recipe. It updates Rust versions in the following files through out the
project:
- .gitlab-ci.yml
- Cargo.toml
- clippy.toml
- rust-toolchain.toml

- Checks whether there are any changes of the working copy or in git
  index and refuse to proceed if any asking to commit or stash them.
- Checks current git branch if it does not contains mentions of current stable
  Rust version (with underscores instead of dots) it interactively propose to
  create a new branch.
- Pulls current stable Rust version from
  https://static.rust-lang.org/dist/channel-rust-stable.toml to upgrade
  to it if not specific version was given, i.e. `just upgrade-rust 1.90`
  upgrades version to 1.90 no metter what the current stable Rust
  version is at the moment.
- Upgrades each of the place with where Rust version is used to pulled
  current stable Rust version outputing details about upgrade:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
- Interactively asks whether to commit the changes and if agreed commits them
  to git with detailed message of what was upgraded and to which version.
- Reminds about the need to fix possible compilation and linting errors and
  warnings after upgrade.

Implementation:

Functionality is delivered through main public "upgrade-rust" just
recipe and set of private (starts with underscore) recipes. Private
recipes still can be called from command line
(i.e. for debugging purposes) but they are not listed in the list of
tasks when you type `just` or `just --list`.
For example, you can still call `just _rust-stable-version` to see what
is current stable version of Rust which is retrieved by script.

"upgrade-rust" has dependency (or pre-dependency)
"_ensure-no-vcs-changes" recipe to check for absence of code changes and
post-dependency "_upgrade-rust-fixes-reminder" for the reminder of
compilation and linting fixes.
The main body of "upgrade-rust" recipe extract current versions of Rust
from number of files (called OLD in the code) to be able to output
upgrade messages of what was upgraded.
For this it uses the following private recipes:
- _current-ci-rust-version
- _current-cargo-rust-version
- _current-clippy-rust-version
- _current-toolchain-rust-version
Each of that recipes (with help of variables) encodes specifics of what, how
and from which file should be retrieved.

"_upgrade-rust-git-create-branch" makes check for the current git branch
and interactively propose to create new one switching to it.

The main workflow of version upgrade in different files is in
"_upgrade-rust" private recipe
(yep, there is upgrade-rust user faced recipe and _upgrade-rust a private one).
"_upgrade-rust" is supplied with specific values to upgrade Rust version
e.g. in .gitlab-ci.yml or in rust-toolchain.toml.
So, "upgrade-rust" calls the following tasks
- _upgrade-rust-ci
- _upgrade-rust-cargo
- _upgrade-rust-clippy
- _upgrade-rust-toolchain
to do the upgrade which supply details to "_upgrade-rust" and call it.

After changes of version "upgrade-rust" calls "_upgrade-rust-git-commit"
recipe to commit changes supplying all the details about upgrade, e.g.
old versions, new version and list of files where Rust version was
changed. "_upgrade-rust-git-commit" asks whether you want to commit
changes, if you agree it makes up a commit message and produce commit.

Functionality of editing quite heavily rely on "sed" utility with some
help from tq (tomlq).
For VCS related operations like branch creation, commiting "git" CLI is
used.

Extension:

The implementation design makes it fairly easy to extend recipes to
support new places where Rust has to be upgraded or remove such support
if some files was removed from source tree.

To add support for new place to perform upgrade:
- Add bunch of variables to justfile, e.g.
  CARGO_FILE_NAME := 'Cargo.toml'
  CARGO_FILE_PATH := absolute_path(CARGO_FILE_NAME)
  CARGO_RUST_VERSION_QUERY := 'package.rust-version'

  Addition of CARGO_RUST_VERSION_QUERY depends on format of your file
  and how you will implement your "_current-cargo-rust-version".

  Obviously replace CARGO/cargo in names of variables and recipes with
  your place name.
- Add "_current-cargo-rust-version" recipe to retrieve currently used
  Rust version from your location, rename the function according to the
  new place.
- Add place specific upgrade recipe named properly, .e.g "_upgrade-rust-cargo"
  which calls to "_upgrade-rust" and supply all the required arguments like
  file path, file name, target version, sed command to change the
  version and some parts of the messages to make proper reporting about
  upgrade process.
- Add newly added recipes to the for-loops in "upgrade-rust" recipe, so
  they can be called.
- Add newly added FILE_PATH and FILE_NAME variables to the call of
  "_upgrade-rust-git-commit" in "upgrade-rust" recipe, so new place can
  be listed in git commit and the changes in it can be added to the
  commit.

Changes:
- Extend list of dependencies in README.md with command line tools
  required for "upgrade-rust" recipe:
  - curl
  - sed
  - tq (from `tomlq` crate)
  - git
- Refer to "upgrade-rust" recipe in newly added "Contribution/Housekeeping"
  sesion of README.md.
- Add bunch of variables to justfile related to number of private
  recipes to compound functionality of "upgrade-rust":
  - SED_RUST_VERSION_REGEX
  - RUST_MANIFEST_STABLE_TOML_URL
  - RUST_MANIFEST_STABLE_VERSION_QUERY
  - RUST_MANIFEST_STABLE_VERSION_PARSE_REGEX
  - DEFAULT_RUST_STABLE_VERSION_FORMAT
  - GITLAB_CI_FILE_NAME
  - GITLAB_CI_FILE_PATH
  - CARGO_FILE_NAME
  - CARGO_FILE_PATH
  - CARGO_RUST_VERSION_QUERY
  - CLIPPY_FILE_NAME
  - CLIPPY_FILE_PATH
  - CLIPPY_RUST_VERSION_QUERY
  - TOOLCHAIN_FILE_NAME
  - TOOLCHAIN_FILE_PATH
  - TOOLCHAIN_RUST_VERSION_QUERY
  - GIT_BRANCH_NAME_PREFIX
- Add "upgrade-rust" recipe which upgrade Rust version in the following
  files:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
- Add private just recipes to delivery different aspects of compound
  functionality of "upgrade-rust" recipe:
  - _ensure-no-vcs-changes
  - _upgrade-rust-git-create-branch
  - _upgrade-rust-fixes-reminder
  - _upgrade-rust-git-commit
  - _upgrade-rust
  - _upgrade-rust-ci
  - _upgrade-rust-cargo
  - _upgrade-rust-clippy
  - _upgrade-rust-toolchain
  - _rust-stable-version
  - _current-ci-image
  - _current-ci-rust-version
  - _current-cargo-rust-version
  - _current-clippy-rust-version
  - _current-toolchain-rust-version
2025-09-28 08:06:39 +00:00
Zeke Fast
62a6248b29 Add docs for tests modules: common, common::assert, common::test.
This is to better explain their purpose.
2025-09-28 01:27:27 +00:00
Zeke Fast
3bb755d8d9 Split hagrid::web::tests::maintenance() to individual tests and move them to modules with route declarations.
Changes:
- Split hagrid::web::tests::maintenance() tests to the following:
  - hagrid::routes::vks:tests::get_upload:::maintenance()
  - hagrid::routes::manage::tests::get_manage::maintenance()
  - hagrid::routes::vks:tests::get_verify_token:::maintenance()
  - hagrid::routes::pks::tests::post_pks_add_multipart_form_data::maintenance()
  - hagrid::routes::api::rest::vks::tests::post_vks_v1_upload::maintenance()
  - hagrid::routes::api::rest::vks::tests::post_vks_v1_request_verify_json::maintenance()
  - hagrid::routes::vks::tests::put_root::maintenance()
- Fix tests' endpoints to make them more accurate and actually match
  existing endpoints:
  - GET /verify -> GET /verify/<token>
  - GET /pks/add -> POST /pks/add/ [Content-Type: multipart/form-data]
  - GET /vks/v1/upload -> POST /vks/v1/upload
  - GET /vks/v1/request-verify -> POST /vks/v1/request-verify
- Verify that maintenance state has dissapeared after maintenance file
  removal in each individual test with the same endpoint on which we
  asserted maintenance mesage.
  Previous tests checked only maintenance message disappearance on GET /upload,
  which wasn't completely correct.
- Introduce new fixtures at hagrid::routes::tests::common:
  - token
  - serialized_cert
  - key_multipart_form_data
- Introduce new test module: hagrid::routes::tests::common::test.
  The module is going to accumulate common tests which define common
  workflow for the test and can be called from other tests to check for
  specific behaviour of concreate function or HTTP endpoint.
- Generalize functionality of maintenance test. Move it to module with
  shared tests: hagrid::routes::tests::common::test::maintenance().
2025-09-28 01:27:27 +00:00
Zeke Fast
8e8cb34522 Extract maintenance_text to a fixture to able to reuse it in different tests. 2025-09-28 01:27:27 +00:00
Zeke Fast
9ad13a30a0 Rename check_maintenance() helper to response() to make usage more ergonomic.
I think usage of assert::maintenance::response() looks much better then
assert::maintenance::check_maintenance() or any other variation.
2025-09-28 01:27:27 +00:00
Zeke Fast
63a4445c9f Move hagrid::web::tests::common::assert::check_maintenance() helper to hagrid::routes::tests::common::assert::maintenance::check_maintenance().
Additional changes:
- Adjust imports and usages accordingly.
2025-09-28 01:27:27 +00:00
Zeke Fast
3ce6e8b495 Refactor hagrid::web::tests::maintenance() tests. Remove tests' code duplication.
Changes:
- Extract and generalize assert for absence of the maintenance text from
  hagrid::web::tests::maintenance() test to
  hagrid::routes::tests::common::assert::response_does_not_contain_text()
  assert helper function. Reuse it in the test instead.
- Refactor hagrid::web::tests::common::assert::check_maintenance()
  function to make it more reusable and remove code duplication in its
  implementation:
  - Instead of duplicating response asserting code call for the helper
    hagrid::routes::tests::common::assert::response() and prefill the
    status.
  - Make it accept response instead of passing in client and uri. This
    make check_maintenance() helper more reusable as now we are not
    dependent on what HTTP verb is used to get the response. In
    addition, request occurs directly inside body of the test and helper
    just assert the response. Hence, reparation of responsibilities and
    following SRP.
  - Let check_maintenance() accept the maintenance text which is checked
    in response. The previous implementation of the
    hagrid::web::tests::maintenance() test and the helper was frigile as
    it allows a lot of possibilities for maintenance text to go out of
    coherence.
- Adjust code in hagrid::web::tests::maintenance() test according to
  changed signature of check_maintenance() assertion helper.
- Remove hardcoded checks in hagrid::web::tests::maintenance() test and
  reuse assertion helpers.
2025-09-28 01:27:27 +00:00
Zeke Fast
bf67b3714e Split and move closer to routes declaration tests for hagrid::web::tests::check_response().
Changes:
- Move tests of hagrid::web::tests::check_response() for /search
  endpoint to hagrid::routes::vks::tests::get_search::not_supported_search_query().
  Refactor tests along the way to get rid of boilerplate code.
- Move test of hagrid::web::tests::check_response() for /pks/lookup
  endpoint to hagrid::routes::pks::tests::get_pks_lookup::not_supported_search_query().
  Refactor tests along the way to get rid of boilerplate code.
- Move test of hagrid::web::tests::check_response()  for
  /.well-known/openpgpkey/<domain>/<policy> endpoint to
  hagrid::routes::wkd::tests::get_wkd_policy::wkd_policy_respond_successfully_with_empty_body().
  Refactor tests along the way to get rid of boilerplate code.
2025-09-28 01:27:27 +00:00
Zeke Fast
f54d6ff283 Replace usage of hagrid::web::tests::common::assert::check_response() with hagrid::routes::tests::common::assert::response() to remove code duplication in tests.
Changes:
- Replace usage of hagrid::web::tests::common::assert::check_response()
  with hagrid::routes::tests::common::assert::response() to remove code
  duplication in tests.
- Refactor hagrid::web::tests::check_response() tests to use
  assert::response() helper.
- Take &str instead of &'static str as present_page_text argument in
  assert::response() helper. This was a bug. It affected reusability of
  the assertion helper function.
2025-09-28 01:27:27 +00:00
Zeke Fast
766e97107e Remove pub visibility modifier from "rocket" fixture.
"rocket" fixture ATM used only by "client" fixture. So, it can be
internal to the hagrid::routes::tests::common module. Its export caused
clash of names with "rocket" module from "rocket" crate which forced to
prefix module from crate with ::, i.e. ::rocket.
Using absolute path to the module is probably a good thing by itself
when it used consistently across code base, but in Hagrid's code base it
wasn't and being forced to do that by that tricky coincidence was
annoying and confusing.
2025-09-28 01:27:27 +00:00
Zeke Fast
7f6c4f88aa Move hagrid::web::tests::basic_consistency() test to hagrid::routes::tests module. Move fixtures declarations to hagrid::routes module.
Changes:
- Nest previously incorrectly placed assert module into common module:
  hagrid::routes::tests::assert -> hagrid::routes::tests::common::assert.
- Move hagrid::web::tests::basic_consistency() test to hagrid::routes module.
- Move and rename assertion helper:
  hagrid::web::tests::common::assert::assert_consistency() ->
  hagrid::routes::tests::common::assert::consistency().
- Move fixtures used in routes testing from hagrid::web::tests::common
  module to hagrid::routes::tests::common:
  - base_uri
  - base_uri_onion
  - cert_name
  - alt_cert_name
  - cert
  - configuration
  - rocket
  - client
- Fix imports and usages of moved functions accordingly.
2025-09-28 01:27:27 +00:00
Zeke Fast
6e7fb88000 Introduce tests' response assertion helper: hagrid::routes::tests::assert::response(). Refactor tests to use it.
Changes:
- Introduce tests' response assertion helper:
  hagrid::routes::tests::assert::response().
- Rename hagrid::routes::about::tests::get_about::about_translation() to
  landing_page_is_visible_with_translations().
- Refactor the following tests to use new assertion helper:
  - hagrid::routes::about::tests::get_about::landing_page_is_visible_with_translations()
  - hagrid::routes::about::tests::get_about::privacy_policy_is_visible()
  - hagrid::routes::about::tests::get_about_privacy::privacy_policy_is_visible()
  - hagrid::routes::about::tests::get_about_api::api_docs_are_visible()
  - hagrid::routes::index::tests::get_root::landing_page_is_visible()
  - hagrid::routes::manage::tests::get_manage::delete_form_is_visible()
  - hagrid::routes::vks::tests::get_upload::upload_form_is_visible()
2025-09-28 01:27:27 +00:00
Zeke Fast
1796989bc3 Move case for "GET /manage" (delete_form_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::manage::tests::get_manage module. 2025-09-28 01:27:27 +00:00
Zeke Fast
e389e64c07 Move case for "GET /upload" (upload_form_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::vks::tests::get_upload module. 2025-09-28 01:27:27 +00:00
Zeke Fast
86b89ac7bc Move case for "GET /about/api" (api_docs_are_visible) of hagrid::web::tests::basics() test to hagrid::routes::about::tests::get_about_api module. 2025-09-28 01:27:27 +00:00
Zeke Fast
5720dbe454 Move case for "GET /about/privacy" (privacy_policy_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::about::tests::get_about_privacy module. 2025-09-28 01:27:27 +00:00
Zeke Fast
20ebdbd0e2 Move case for "GET /about" (privacy_policy_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::about::tests::get_about module. 2025-09-28 01:27:27 +00:00
Zeke Fast
090a6f222a Move case for "GET /" (landing_page_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::index::tests::get_root module. 2025-09-28 01:27:27 +00:00
Zeke Fast
74c25c9d9b Move hagrid::web::tests::about_translation() to hagrid::routes::about::tests::get_about module. 2025-09-28 01:27:27 +00:00
Zeke Fast
07804b8833 Refactor hagrid::web::tests::basics() test to use table testing with rstest.
There several caveats to the refactoring:
- Before refactoring all tests ran in strong order as they were
  sequential code in a single function. I believe this was an accident
  behaviour and there were nothing in functionality of basics() fn that
  forced certain sequence of test execution. So, using table testing
  with random order considered by me fine.
- basics() fn tests only read queries. So, it was a discovery for me to
  find at the end of the function check for consistency,
  (i.e. assert_consistency()) as it logically does not make sense.
  I believe that was a test code copy-paste mistake.
  To preserve basic check I moved it to dedicated test basic_consistency().
2025-09-28 01:27:27 +00:00
Zeke Fast
8795469b52 Format code. 2025-09-28 01:27:27 +00:00
Zeke Fast
29ac3534c1 Collapse similar tests into check_response() fn using table testing. 2025-09-28 01:27:27 +00:00
Zeke Fast
b6ad3f3705 Use "base_uri" fixture instead of BASE_URI const. Eliminate usage of BASE_URI const. 2025-09-28 01:27:27 +00:00
Zeke Fast
d9741fad8f Introduce hagrid::web::tests::common::assert module. Move helper assertion fns there.
Changes:
- Move the following functions from hagrid::web::tests module to
  hagrid::web::tests::common::assert:
  - assert_consistency
  - check_maintenance
  - check_null_response
  - check_null_responses_by_email
  - check_responses_by_email
  - check_mr_response
  - check_index_response
  - check_mr_responses_by_fingerprint
  - check_response
  - check_hr_response
  - check_hr_response_onion
  - check_hr_responses_by_fingerprint
  - check_wkd_response
  - check_verify_link
  - check_verify_link_json
  - check_mails_and_verify_email
  - check_mails_and_confirm_deletion
  - pop_mail_capture_pattern
  - vks_publish_submit_multiple
  - vks_publish_submit_get_token
  - vks_publish_submit_response
  - vks_publish_shortcut_get_token
  - vks_publish_json_get_token
  - vks_manage
  - vks_manage_delete
- Change calls to these helper functions around the code accordingly.
2025-09-28 01:27:27 +00:00
Zeke Fast
76ec3eed82 Refactor helper functions in tests of hagrid::web module into fixtures.
Fixtures allow for push-style cascading dependency resolution with
ability to override some of the dependent fixture's arguments when needed.
This is very nice as it gives a lot of flexibility and suitable in a lot
of case. So, single fixture based approach can be applied to most of the
tests.

In addition, this result in good reusability of test code.

If you feel overwelmed and confused with complex argument list of test
functions try to read links from "Documentation" section (see below).

But here is the primer.

    #[fixture]
    pub fn configuration(
        base_uri: &str,
        base_uri_onion: &str,
    ) -> (TempDir, :🚀:figment::Figment) {
        // ...
    }

Attribute #[fixture] turn function into fixture which means now it can
be injected into tests or other fixtures by "configuration" argument name.

"base_uri", "base_uri_onion" are other fixtures which are injected into
"configuration".

    #[fixture]
    pub fn rocket(
        #[from(configuration)] (tmpdir, config): (TempDir, :🚀:figment::Figment),
    ) -> (TempDir, :🚀:Rocket<:🚀:Build>) {
        // ...
    }

As we destructuring results of "configuration" fixture injection into
"rocket" fixture the name of the fixture can't be inferenced from
parameters name, so we have to give a hint with #[from(configuration)]
which fixture has to be called.

    #[rstest]
    fn hkp_add_two(
        base_uri: &str,
        #[from(cert)] (_, tpk_0): (&str, Cert),
        #[with(alt_cert_name())]
        #[from(cert)]
        (_, tpk_1): (&str, Cert),
        #[from(client)] (tmpdir, client): (TempDir, Client),
    ) {
        // ...
    }

This is probably the most trickies function signature, but let brake it
down.

There are 4 fixtures injected into "hkp_add_two" test function:
- base_uri
- cert
- cert
- client

I think at this point of explanation syntax for "base_uri" and "client"
fixturex should be clear.

So,
    #[from(cert)] (_, tpk_0): (&str, Cert),
    #[with(alt_cert_name())] #[from(cert)] (_, tpk_1): (&str, Cert),

calls to "cert" fixture which takes "cert_name" argument. Here is the
definition:

    #[fixture]
    pub fn cert<'a>(cert_name: &'a str) -> (&'a str, Cert) {
        // ...
    }

For the "hkp_add_two" test we need two certificates, so first one is
generated with default name which is returned by "cert_name" fixture,
but for the second with #[with(alt_cert_name())] we say that we want to
use "alt_cert_name" fixture for cert_name argument, so the certificate
is generated with "bar@invalid.example.com" name.

And parenthis for destructing of the results of injection as "cert"
fixture returns tuple of (cert_name, cert).

Documentation:
- https://crates.io/crates/rstest
- https://docs.rs/rstest/0.26.1/rstest/attr.fixture.html

NOTE: probably this changes made translation generation in tests more unstable.
It was already unstable, but it looks like the use of fixtures make a
bit more unstable. So, in case of test failures, just run them several
times. I'll take a look and modules compilation order later once move
tests into modules with SUT's functionality.

Changes:
- Turn the following helper functions from hagrid::web::tests
  module to fixtures:
  - configuration()
  - rocket()
  - client()
- Panic immediately in the fixtures and don't bubble up the error like
  it was in helper functions. Bubbling up errors does not make sense as
  we panic with expect later in tests code. So, we duplicated code
  (.unwrap()) calls) and make return signatures more complex.
- Inject "base_uri" and "base_uri_onion" into "configuration" fixture as
  after turning it into fixture we can do that.
- Remove BASE_URI_ONION constant as it is not used. Now "base_uri_onion"
  fixture is used instead.
- Refactor all the tests which used mentioned above helper functions to
  use fixture.
2025-09-28 01:27:27 +00:00
Zeke Fast
0d868ce27e Replace "build_cert" helper fn in tests of hagrid::web module with "cert" fixture.
Changes:
- Turn "build_cert" funtion into fixture.
- Refactor all tests which relied on build_cert() to inject one or two
  certs.
- Replace usage of duplicated e-mail (cert_name) in tests with value
  returned by "cert" fixture (i.e. cert_name). This better align actual
  cert and cert_name usage in tests and eliminate hardcoded e-mail
  string slices.
2025-09-28 01:27:27 +00:00
Zeke Fast
d32b48885e Declare tests in hagrid::web module with #[rstest] instea of #[test].
This should allow to use all the rstest goodies with these tests.
2025-09-28 01:27:27 +00:00
Zeke Fast
b11f7dc7b3 Extract BASE_URI and BASE_URI_ONION to rstest::fixture's. Use fixture injection in tests.
Changes:
- Create fixtures based on constants:
  - base_uri
  - base_uri_onion
- Change tests which use BASE_URI or BASE_URI_ONION from #[test]
  declaration to #[rstest]. That allows to inject fixtures.
- Replace usage of constants BASE_URI and BASE_URI_ONION with base_uri
  and base_uri_onion fixtures.
  There are still some usages of the constants which will be refactored
  later.
- Propagate injected fixture values into check_* assertions to replace
  constant usages.
2025-09-28 01:27:27 +00:00
Zeke Fast
f8c4871b61 Add "rstest" as dev-dependency to hagrid's Cargo.toml.
Additional changes:
- Update Cargo.lock: `just build`
2025-09-28 01:27:27 +00:00
Zeke Fast
93aa79e979 Remove outdated comment in clippy.toml.
Hagrid has switched to 1.89 and the comment does not make any sense
anymore.
2025-09-28 01:18:44 +00:00
Zeke Fast
0fe99ba962 Fix linting errors after Rust version upgrade.
Command: just lint

Changes:
- Fix the following linting errors:

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:335:5
        |
    335 | /     if let Ok(target) = read_link(link) {
    336 | |         if target == expected {
    337 | |             remove_file(link)?;
    338 | |         }
    339 | |     }
        | |_____^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
        = note: `#[warn(clippy::collapsible_if)]` on by default
    help: collapse nested if block
        |
    335 ~     if let Ok(target) = read_link(link)
    336 ~         && target == expected {
    337 |             remove_file(link)?;
    338 ~         }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:510:9
        |
    510 | /         if let Ok(target) = read_link(&link_fpr) {
    511 | |             if target == expected {
    512 | |                 remove_file(&link_fpr)?;
    513 | |             }
    514 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    510 ~         if let Ok(target) = read_link(&link_fpr)
    511 ~             && target == expected {
    512 |                 remove_file(&link_fpr)?;
    513 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:515:9
        |
    515 | /         if let Ok(target) = read_link(&link_keyid) {
    516 | |             if target == expected {
    517 | |                 remove_file(link_keyid)?;
    518 | |             }
    519 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    515 ~         if let Ok(target) = read_link(&link_keyid)
    516 ~             && target == expected {
    517 |                 remove_file(link_keyid)?;
    518 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:630:9
        |
    630 | /         if let Ok(link_fpr_target) = link_fpr.canonicalize() {
    631 | |             if !link_fpr_target.ends_with(&path_published) {
    632 | |                 info!(
    633 | |                     "Fingerprint points to different key for {} (expected {:?} to be suffix of {:?})",
    ...   |
    638 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    630 ~         if let Ok(link_fpr_target) = link_fpr.canonicalize()
    631 ~             && !link_fpr_target.ends_with(&path_published) {
    632 |                 info!(
    ...
    636 |                 return Err(anyhow!(format!("Fingerprint collision for key {}", fpr)));
    637 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:640:9
        |
    640 | /         if let Ok(link_keyid_target) = link_keyid.canonicalize() {
    641 | |             if !link_keyid_target.ends_with(&path_published) {
    642 | |                 info!(
    643 | |                     "KeyID points to different key for {} (expected {:?} to be suffix of {:?})",
    ...   |
    648 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    640 ~         if let Ok(link_keyid_target) = link_keyid.canonicalize()
    641 ~             && !link_keyid_target.ends_with(&path_published) {
    642 |                 info!(
    ...
    646 |                 return Err(anyhow!(format!("KeyID collision for key {}", fpr)));
    647 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/lib.rs:534:9
        |
    534 | /         if let Some(current_fpr) = current_link_fpr {
    535 | |             if current_fpr != *fpr_primary {
    536 | |                 self.set_email_unpublished_filter(tx, &current_fpr, |uid| {
    537 | |                     Email::try_from(uid)
    ...   |
    542 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    534 ~         if let Some(current_fpr) = current_link_fpr
    535 ~             && current_fpr != *fpr_primary {
    536 |                 self.set_email_unpublished_filter(tx, &current_fpr, |uid| {
    ...
    540 |                 })?;
    541 ~             }
        |

    warning: this `if` statement can be collapsed
      --> hagridctl/src/delete.rs:79:9
       |
    79 | /         if err.is_ok() {
    80 | |             if let Err(e) = result {
    81 | |                 err = Err(e);
    82 | |             }
    83 | |         }
       | |_________^
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
       = note: `#[warn(clippy::collapsible_if)]` on by default
    help: collapse nested if block
       |
    79 ~         if err.is_ok()
    80 ~             && let Err(e) = result {
    81 |                 err = Err(e);
    82 ~             }
       |

    warning: this `if` statement can be collapsed
       --> hagridctl/src/import.rs:197:9
        |
    197 | /         if !acc.is_empty() {
    198 | |             if let Packet::PublicKey(_) | Packet::SecretKey(_) = packet {
    199 | |                 callback(acc);
    200 | |                 acc = vec![];
    201 | |             }
    202 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    197 ~         if !acc.is_empty()
    198 ~             && let Packet::PublicKey(_) | Packet::SecretKey(_) = packet {
    199 |                 callback(acc);
    200 |                 acc = vec![];
    201 ~             }
        |

    warning: this `if` statement can be collapsed
       --> src/dump.rs:522:25
        |
    522 | /                         if pd.mpis {
    523 | |                             if let Ok(ciphertext) = e.ciphertext() {
    524 | |                                 pd.dump_mpis(output, &ii, &[ciphertext], &["ciphertext"])?;
    525 | |                             }
    526 | |                         }
        | |_________________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
        = note: `#[warn(clippy::collapsible_if)]` on by default
    help: collapse nested if block
        |
    522 ~                         if pd.mpis
    523 ~                             && let Ok(ciphertext) = e.ciphertext() {
    524 |                                 pd.dump_mpis(output, &ii, &[ciphertext], &["ciphertext"])?;
    525 ~                             }
        |
2025-09-28 01:18:44 +00:00
Zeke Fast
0dceaa454f Fix compilation warnings after Rust version upgrade.
Command: `just check`

Changes:
- The following warnings were fixed:

    warning: hiding a lifetime that's elided elsewhere is confusing
       --> database/src/sqlite.rs:146:11
        |
    146 |     fn tx(&self) -> &Transaction {
        |           ^^^^^     ------------
        |           |         ||
        |           |         |the same lifetime is hidden here
        |           |         the same lifetime is elided here
        |           the lifetime is elided here
        |
        = help: the same lifetime is referred to in inconsistent ways, making the signature confusing
        = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
    help: use `'_` for type paths
        |
    146 |     fn tx(&self) -> &Transaction<'_> {
        |                                 ++++

    warning: hiding a lifetime that's elided elsewhere is confusing
      --> src/dump.rs:22:30
       |
    22 |     pub fn display_sensitive(&self) -> SessionKeyDisplay {
       |                              ^^^^^     ----------------- the same lifetime is hidden here
       |                              |
       |                              the lifetime is elided here
       |
       = help: the same lifetime is referred to in inconsistent ways, making the signature confusing
       = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
    help: use `'_` for type paths
       |
    22 |     pub fn display_sensitive(&self) -> SessionKeyDisplay<'_> {
       |                                                         ++++
2025-09-28 01:18:44 +00:00
Zeke Fast
6060fcf0bc Upgrade Debian image version in CI: bookworm -> trixie.
Debian 13.0 (codename: trixie) was released on 2025-08-09.

Changes:
- Update version of the used CI image: 1.89-bookworm -> 1.89-trixie
2025-09-28 01:18:44 +00:00
Zeke Fast
3285f19d09 Upgrade Rust toolchain: 1.86 -> 1.89
If you don't have toolchain installed and you use rustup run:

    $ rustup install --component rustfmt --component clippy 1.89

NOTE: It might be that you have 1.89.0 installed as stable toolchain, in
that case you still have to run the above command to install exactly
1.89.

Changes:
- Upgrade version of used toolchain in the following places:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
2025-09-28 01:18:44 +00:00
Zeke Fast
82f48d3a4e Code review issue: Add a reminder comment to hook up initializers for execution. 2025-09-27 22:54:01 +02:00
Zeke Fast
5f819e8004 Code review issue: Reorder handlers in hagrid::routes::api::rest::vks module.
This it to make order to more organized and logical.
2025-09-27 22:42:30 +02:00
Zeke Fast
d7b5796abf Fix translation generation. 2025-08-23 12:49:59 +02:00
Zeke Fast
5beafb2881 Format code. 2025-08-23 07:42:20 +02:00
Zeke Fast
d8fcaa7d5e Introduce idea of initalizers. Extract rocket bootstrap logic from hagrid::web and rocket_factory() function to initializers.
Continue to dissolve hagrid::web module which had way too much responsibilities
and became the god-module (see https://en.wikipedia.org/wiki/God_object).

In addition, solved and refined several smaller issues and made
improvements.

Changes:
- Extract configuration loading (i.e. extraction) from
  hagrid::web::rocket_factory() to hagrid::config::load() and call it
  from hagrid::app::configure_rocket().
- Extract and split hagrid::web::run() (which previously was #[launch] on
  rocket() fn) to hagrid::app::run(), hagrid::app::configure_rocket()
  and hagrid::app::run_rocket().
- Remove String copy in Configuration::base_uri_onion() and return
  references to internal fields of the struct. Callers can copy it when
  they need to, otherwise they can just use references without overhead.
- Extract rocket() helper function from client() in tests of hagrid::web
  module to separate rocket instance creation.
- Remove code duplication in tests of hagrid::web module by reusing
  rocket() and client() functions for instances construction.
- Introduce idea of initializers and "hagrid::initializers" module to bootstrap
  rocket framework and register states (with rocket.manage()), fairings (with
  rocket.attach()) and routes (with rocket.mount()) on rocket instance.

  A single initializer resides in submodule of hagrid::initializers and
  register itself in hagrid::initializers::run() function (has to be
  done manually by the programmer who adds new initalizer).
  Initializer consist of the following two functions with some vary signatures:
  - init(config: &Configuration) -> hagrid::initalizers::Return<CONSTRUCTED_TYPE>
  - register(rocket: Rocket<Build>, config: &Configuration, state_fairing_or_routes: <CONSTRUCTED_TYPE>) -> Rocket<Build>

  init(config: &Configuration)
  The function instantiate state, fairing or routes possibly using
  config for it.
  Tricky moment about it is its return type which has to be
  hagrid::initializers::Result. This return type is an alias to
  anyhow::Result<Option<T>>. When construction fails for whatever reason it
  returns Err. If everything went well, but construction is not a desired
  behaviour, i.e. because of it was disabled in configuration, like we
  do for "prometheus" initializer init() should return Ok(None).
  Errors then handled in hagrid::initializers::run() function (by
  initialize!() macro to be precise) and register the instance if it was
  created.

  register(rocket: Rocket<Build>, config: &Configuration, state_fairing_or_routes: <CONSTRUCTED_TYPE>)
  Receives unwrapped instance of state, fairing or routes if it was
  constructed and register it on rocket.
  Some instances can be registered several times, e.g. like as fairing
  and as routes. We do that for PrometheusMetrics.
- Move hagrid::web::ApplicationState (what was HagridState before
  renaming) to hagrid::app::state::ApplicationState.
- Dissolve hagrid::web::rocket_factory() and set of configure_*
  functions into initializers preserving the order of their registration
  on rocket, though it might be not so important.
- Replace usage of hagrid::web::rocket_factory() in hagrid::web
  module's tests with hagrid::app::configure_rocket() which become
  approximate functional equivalent of it but calls for initializers to
  populate the rocket.
2025-08-23 06:39:27 +02:00
Zeke Fast
fb0d0c24c4 Rename hagrid::web::HagridState to ApplicationState. 2025-08-21 17:55:41 +02:00
Zeke Fast
34d056ea55 Extract route handlers from hagrid::web to newly added hagrid::routes module.
hagrid::web module was and is highly overloaded with functionality and
contains all kind of code from business logic to http handling.

With this commit I tried to offload complexity of handler to a dedicated
module to make reasoning about available endpoints a bit easier.

Changes:
- Move the following endoints to hangrid::routes module and its newly
  added submodules:
  - hagrid::web::routes()                               -> hagrid::routes::routes()
  - hagrid::web::root()                                 -> hagrid::routes::index::root()
  - hagrid::web::about()                                -> hagrid::routes::about::about()
  - hagrid::web::news()                                 -> hagrid::routes::about::news()
  - hagrid::web::news_atom()                            -> hagrid::routes::atom::news_atom()
  - hagrid::web::privacy()                              -> hagrid::routes::about::privacy()
  - hagrid::web::apidoc()                               -> hagrid::routes::about::apidoc()
  - hagrid::web::faq()                                  -> hagrid::routes::about::faq()
  - hagrid::web::usage()                                -> hagrid::routes::about::usage()
  - hagrid::web::files()                                -> hagrid::routes::assets::files()
  - hagrid::web::stats()                                -> hagrid::routes::about::stats()
  - hagrid::web::errors()                               -> hagrid::routes::errors::errors()
  - hagrid::web::vks_api::vks_v1_by_email()             -> hagrid::routes::api::rest::vks::vks_v1_by_email()
  - hagrid::web::vks_api::vks_v1_by_fingerprint()       -> hagrid::routes::api::rest::vks::vks_v1_by_fingerprint()
  - hagrid::web::vks_api::vks_v1_by_keyid()             -> hagrid::routes::api::rest::vks::vks_v1_by_keyid()
  - hagrid::web::vks_api::upload_json()                 -> hagrid::routes::api::rest::vks::upload_json()
  - hagrid::web::vks_api::upload_fallback()             -> hagrid::routes::api::rest::vks::upload_fallback()
  - hagrid::web::vks_api::request_verify_json()         -> hagrid::routes::api::rest::vks::request_verify_json()
  - hagrid::web::vks_api::request_verify_fallback()     -> hagrid::routes::api::rest::vks::request_verify_fallback()
  - hagrid::web::vks_web::search()                      -> hagrid::routes::vks::search()
  - hagrid::web::vks_web::upload()                      -> hagrid::routes::vks::upload()
  - hagrid::web::vks_web::upload_post_form()            -> hagrid::routes::vks::upload_post_form()
  - hagrid::web::vks_web::upload_post_form_data()       -> hagrid::routes::vks::upload_post_form_data()
  - hagrid::web::vks_web::request_verify_form()         -> hagrid::routes::vks::request_verify_form()
  - hagrid::web::vks_web::request_verify_form_data()    -> hagrid::routes::vks::request_verify_form_data()
  - hagrid::web::vks_web::verify_confirm()              -> hagrid::routes::vks::verify_confirm()
  - hagrid::web::vks_web::verify_confirm_form()         -> hagrid::routes::vks::verify_confirm_form()
  - hagrid::web::vks_web::quick_upload()                -> hagrid::routes::vks::quick_upload()
  - hagrid::web::vks_web::quick_upload_proceed()        -> hagrid::routes::vks::quick_upload_proceed()
  - hagrid::web::debug_web::debug_info()                -> hagrid::routes::debug::debug_info()
  - hagrid::web::hkp::pks_lookup()                      -> hagrid::routes::pks::pks_lookup()
  - hagrid::web::hkp::pks_add_form()                    -> hagrid::routes::pks::pks_add_form()
  - hagrid::web::hkp::pks_add_form_data()               -> hagrid::routes::pks::pks_add_form_data()
  - hagrid::web::hkp::pks_internal_index()              -> hagrid::routes::pks::pks_internal_index()
  - hagrid::web::wkd::wkd_policy()                      -> hagrid::routes::wkd::wkd_policy()
  - hagrid::web::wkd::wkd_query()                       -> hagrid::routes::wkd::wkd_query()
  - hagrid::web::manage::vks_manage()                   -> hagrid::routes::manage::vks_manage()
  - hagrid::web::manage::vks_manage_key()               -> hagrid::routes::manage::vks_manage_key()
  - hagrid::web::manage::vks_manage_post()              -> hagrid::routes::manage::vks_manage_post()
  - hagrid::web::manage::vks_manage_unpublish()         -> hagrid::routes::manage::vks_manage_unpublish()
  - hagrid::web::maintenance::maintenance_error_web()   -> hagrid::routes::maintenance::maintenance_error_web()
  - hagrid::web::maintenance::maintenance_error_json()  -> hagrid::routes::maintenance::maintenance_error_json()
  - hagrid::web::maintenance::maintenance_error_plain() -> hagrid::routes::maintenance::maintenance_error_plain()
2025-08-21 16:01:18 +02:00
Zeke Fast
eb4ffd59f4 Collect adhoc configuration extraction into hagrid::app::config module. Introduce Configuration struct.
Adhoc configuration value extraction from Figment is hard to reason on
application level, so it is better to create application wide
configuration. Apart of that having a unified module, like
hagrid::app::config, should allow to consolidate config logic in a
single place and simplify many other places.

Changes:
- Introduce hagrid::app::config module and Configuration struct in it.
- Migrate and consolidate adhoc config value extraction to a single
  struct hagrid::app::config::Configuration.
- Replace logic to calculate default values in configuration in cases
  when values were not provided with functionality from "serde" crate,
  i.e. using #[serde(default)] or #[serde(default = ...)] to fill
  missing values with defaults.
- Clean up signatures of configure_* like functions in hagrid::web::
  module as some of them after moving configuration extraction can not
  fail any more.
- Replace configuration propagation from pull to push model in
  configure_* functions. Before the refactoring they all depend on
  figment and configuration has been pulled from figment and put into
  modules, now we explicitly push it. That's reduce dependency on global
  configuration. That still might be reworked again once we move module
  creation to hagrid::app module from hagrid::web.
2025-08-21 12:36:23 +02:00
Zeke Fast
5ed05975e7 Extract routes from local variable in rocket_factory() function to routes().
Routes are supposed to change from time to time extraction to a
dedicated function isolates them from rocket setup code in
rocket_factory() function.
2025-08-20 23:10:23 +02:00
Zeke Fast
9b6b495f56 Extract modules declaration from main.rs to lib.rs in "hagrid" crate.
This is to make "hagrid" code library alike instead of having it as a binary
which improves reusability and testability of the code.

So, the main.rs binary will be just a launcher for library code.

Changes:
- Extract modules declaration from main.rs to lib.rs in "hagrid" crate.
- Introduce "app" module which has to accumulate infrustructure code,
  but for the time being it delegates run() and handle errors.
- Rename hagrid::web::serve() to hagrid::web::run() to standardize on
  module entry function names.
- Accumulate rocket framework launching code in hagrid::web::run()
  function.
  To be able to move code around I have to replace usage of
  macros like #[rocket::launch] or #[rocket::main] with direct usage of
  :🚀:async_main, ignite() and launch() functions.
2025-08-20 22:24:40 +02:00
Zeke Fast
9adeb4d544 Clean ups in justfile. 2025-08-20 21:17:36 +02:00
Zeke Fast
015e698725 Clean up unused imports warnings which appeared in tests.
Cleaned warnings:

    warning: unused import: `std::time::SystemTime`
      --> database/src/test.rs:19:5
       |
    19 | use std::time::SystemTime;
       |     ^^^^^^^^^^^^^^^^^^^^^
       |
       = note: `#[warn(unused_imports)]` on by default

    warning: unused import: `PublicParts`
      --> database/src/test.rs:24:55
       |
    24 | use sequoia_openpgp::packet:🔑:{Key4, PrimaryRole, PublicParts, SecretParts};
       |                                                       ^^^^^^^^^^^

    warning: unused imports: `Features` and `HashAlgorithm`
      --> database/src/test.rs:25:30
       |
    25 | use sequoia_openpgp::types::{Features, HashAlgorithm};
       |                              ^^^^^^^^  ^^^^^^^^^^^^^
2025-08-20 02:09:37 +02:00
Vincent Breitmoser
8b89ab112a version 2.1.0 2025-08-01 10:05:50 +02:00
Vincent Breitmoser
7d3194dd25 cargo: cargo update 2025-08-01 10:01:20 +02:00
Wiktor Kwapisiewicz
5aa404fc32 Split tests for: naked key, DKS key, revoked key 2025-07-31 11:51:27 +02:00
Wiktor Kwapisiewicz
5b28cedf37 Fix naked-key upload test (test_no_selfsig)
Previously, the test used Sequoia's high-level API which attached a
Direct-Key Signature to the key. As such, this wasn't a truly
naked-key.

This test uses low-level API to contruct a bare key and checks if the
import fails. Then it attaches a DKS which causes the import to
succeed. Then it revokes that key and checks if the revocation is
correctly propagated.
2025-07-28 13:28:04 +02:00
Wiktor Kwapisiewicz
12f0eef5be Consider keys OK if they have at least one self-signature
This is especially useful for small keys with only a Direct Key Signature.

Closes: https://gitlab.com/keys.openpgp.org/hagrid/-/issues/180
2025-07-24 10:50:01 +02:00
Daniel Huigens
94bf37a6a3 Add logo 2025-07-10 16:26:53 +02:00
Vincent Breitmoser
ce8a6deed0 nix: add hagridctl package 2025-06-17 10:17:01 +02:00
Vincent Breitmoser
df221eaf2b nix: update and fix nix files for new build 2025-06-17 10:09:35 +02:00
Zeke Fast
7532ff4b22 Fix compilation errors after crates upgrade. 2025-05-12 08:15:44 +00:00
Zeke Fast
ca7d0406cf Upgrade rocket and related crates.
Changes:
- Upgrade the following crate dependencies in Cargo.toml:
  - log: 0.4.22 -> 0.4.27
  - rocket_dyn_templates: 0.1.0 -> 0.2.0
  - rocket_prometheus: 0.10.0 -> 0.10.1
  - handlebars: 4.5.0 -> 5.1.2 (indirect dependency of rocket_dyn_templates)
- Update Cargo.lock.
2025-05-12 08:15:44 +00:00
Zeke Fast
784d866da0 Remove aliasing of Sqlite as KeyDatabase in database/src/lib.rs. Change usages accordingly.
I believe KeyDatabase name is the thing of the past which was made to
simplify migration to Sqlite without doing the renaming all over code
base (which this commit does). So, this is a bit of technical debt IMO
and direct usage of Sqlite would serve better code readability.
KeyDatabase leaks presence of Sqlite though functions like new_file() and
others as such it doesn't serve very good the purpose of abstracting
things away and hiding the fact of Sqlite behind.
2025-05-12 07:53:18 +02:00
Zeke Fast
6a4e20a41f Remove hagridctl/Rocket.toml config as we don't use it in hagridctl any more. 2025-05-06 17:19:31 +02:00
Zeke Fast
e7b7b994ce Fix punctuation in help messages. 2025-05-06 10:31:32 +02:00
Zeke Fast
4a6ba18b33 Remove unnecessary Result:: type specification and use Ok() directly in database::Sqlite.
Awhile ago we returned default Result type to core::result::Result
instead of anyhow::Result. So, such prefixing is not needed any more.
2025-05-06 10:31:28 +02:00
Zeke Fast
d9afbd151c Replace "-c, --config" and "-e, --environment" opts with "--db-file-path" and "HAGRID_DB_FILE_PATH" env var.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2484117659

Changes:
- Extract database file path and log dir path constructions from
  Sqlite::new_file() and Sqlite::new_internal() associated functions to
  Sqlite::db_file_path() and Sqlite::log_dir_path().
  These path manipulations inside Sqlite/KeyDatabase wasn't ideal as
  they increased diversity of reasons why Sqlite constructions can fail,
  unnecessary constrain how Sqlite and with which paths can be used, and
  that logic doesn't actually belong to Sqlite itself. So, later we
  probably have to move these db_file_path() and log_dir_path()
  functions else where.
- Replace base_dir parameter of Sqlite::new_file() with "db_file" and "log_dir"
  params which gives us a way to use database file passed in command
  line options.
- Add Sqlite::log_dir_path_from_db_file_path() associated function to
  infer log_dir_path based on given db_file_path.
  It is used in command handler where only db_file_path provided.
- Pull up log dir construction from Sqlite::new_internal() to
  Sqlite::new_file() to avoid passing through log_dir_path through
  several functions. This also makes Sqlite::new_file() signature
  cleaner.
- Fix usages of Sqlite/KeyDatabase::new_file through out the code base:
  and instead of providing base_dir, populate db_file_path and
  log_dir_path using Sqlite::db_file_path() and Sqlite::log_dir_path()
  functions which calculate paths based on provided base_dir.
- Remove "-c, --config" and "-e, --environment" options from
  hagridctl::cli::Cli.
- Remove hagridctl::cli::DeploymentEnvironment enum.
- Remove entire hagridctl::config module as we don't extract paths from
  configuration any more.
- Add "env" feature flag to "clap" dependency in hagridctl/Cargo.toml to
  be able to populate Cli::db_file_path field from HAGRID_DB_FILE_PATH
  environment variable.
- Add optional --db-file-path option to "hagridctl". Cli::db_file_path
  is obligatory, but when --db-file-path is not provided Clap tries to
  populate it from HAGRID_DB_FILE_PATH env var and if that's not
  possible Clap terminates with error message.
- In hagridctl::cli::dispatch_cmd() use db_file_path from
  cli.db_file_path instead of keys_internal_dir extracted from configuration.
- Replace keys_db_internal_dir with db_file_path in the following
  command handlers:
  - hagridctl::delete::run
  - hagridctl::import::run
- Get rid of unnecessary Result unwrapping and wrapping it again in
  hagrid::web::configure_db_service function and return the value from
  KeyDatabase::new_file immediately instead.
2025-05-06 10:30:18 +02:00
Zeke Fast
d7de01d023 Add CLI's tests using clap::Command::debug_assert().
Documentation: https://docs.rs/clap/latest/clap/struct.Command.html#method.debug_assert
2025-05-05 01:28:59 +02:00
Zeke Fast
eb36332f8b Validate presence of help text for opts and args of hagridctl and tester bins.
Changes:
- Set "help_expected = true" for the following CLI structs:
  - hagridctl::cli::Cli
  - tester::cli::Cli
- Fill missing help texts.
2025-05-05 01:26:23 +02:00
Zeke Fast
e2594b019c Add "--workspace" flag to "just build" recipe, so it builds all Rust code.
Additional changes:
- In case of necessity specific binary can be provided for "just build"
  recipe, e.g. "just build -p hagrid" or "just build -p hagridctl".
2025-05-05 00:05:23 +02:00
Zeke Fast
81b333bd43 Replace "base" arg in "hagridctl delete" cmd with keys_internal_dir extracted from config.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2482983036

Changes:
- Change type of "base_dir" argument of database::Sqlite::new_file()
  associated function form "impl Into<PathBuf>" to "impl AsRef<Path>"
  as AsRef<Path> is more generic.
- Remove "base" argument from "hagridctl delete" command.
- Extract "config" and "environment" options from "import" command to
  hagridctl::cli::Cli struct.
- Make "config" and "environment" options global, i.e. they can be
  specified with any command.
- Change "environment" from being argument to option as being argument
  doesn't play well with "import" command where arbitrary list of
  KEYRING_FILES can be specified. In general, being global works much
  better with options then arguments.
  So, <ENVIRONMENT> argument became "-e, --environment <ENVIRONMENT>" option
  with the same default value (production).
- KEYRING_FILES in "import" command don't need to be "last" any more.
- Get "keys_db_internal_dir" in hagridctl::cli::dispatch_cmd() and let
  it be propagated to places where KeyDatabase is created instead of
  progapagation of config or base_dir.
2025-05-04 23:37:54 +02:00
Zeke Fast
8fad06e11d Move args related to "import" cmd from top level to "import" command. Clean ups.
Before merging "hagrid-delete" into "hagridctl" the later was
exclusively used with "import" command. So, it doesn't make a difference
how "config" and "ENVIRONMENT" parameters were specified. After merge
these arguments doesn't make sense at the top level any more, hence the
move.

Changes:
- Move "file" and "env" field from hagridctl::cli::Cli struct into
  hagridctl::cli::Command::Import enum variant.
- Because of the move and optionality of "ENVIRONMENT" argument for
  "import" command I had to add "last = true" option to KEYRING_FILES
  argument which requires their specification to be prepended with --.
- Clean up hagridctl::cli::dispatch_cmd() function as match statement
  now can take ownership over cli.command and "file" and "env" fields of
  Command::Import can be destructured and passed directly to
  config::load() instead of passing around reference to Cli struct.
  Overall code becomes cleaner, IMO.
- Change config::load() to take "file" and "env" directly instead of
  "cli" parameter.
- Get rid of run() function in hagridctl/src/main.rs as it becomes
  redundant.
2025-05-04 20:47:53 +02:00
Zeke Fast
9c4b51fa61 Merge "hagrid-delete" binary into "hagridctl" crate.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2482960066

Changes:
- Remove "hagrid-delete" bin declaration from Cargo.toml.
- Remove "clap" dependency for "hagrid" crate in Cargo.toml as after
  moving "hagrid-delete" to "hagridctl" "hagrid" crate does not use
  "clap" any more.
- Remove "run-hagrid-delete" recipe with its "hagrid-delete" and
  "delete" aliases from justfile.
- Update Cargo.lock.
- Create "delete" command for "hagridctl" by adding cli::Command::Delete
  variant which previously were hagrid-delete::cli::Cli struct.
- Move "delete" module with command handler ("run" function) from
  "hagrid" crate to hagridctl::delete.
- Extend hagridctl::cli::dispatch_cmd() function with processing of
  cli::Command::Delete variant and call to hagridctl::delete::run.
- Move print_errors() from hagrid::delete::cli module to hagrdictl::cli.
- Move KeyDatabase instantiation from hagrid/src/main.rs into
  hagridctl::delete::run command handler as this way of database
  instantiation is specific to "delete" command.
  Probably later we have to reconsider how we instantiate database for
  all the commands to avoid reimplementing that functionality every time
  and duplicating the code.
  That move caused change in signature of hagridctl::delete::run
  function. Now we pass base path instead of db reference.
2025-05-04 20:19:02 +02:00
Zeke Fast
9f5d8b3706 Change text in "tester" crate "description" field.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2482945343
2025-05-04 18:04:59 +02:00
Zeke Fast
24034536e8 Clean up linting warnings.
Commands:

    just lint

Warnings:

    warning: this expression creates a reference which is immediately dereferenced by the compiler
      --> hagridctl/src/cli.rs:39:58
       |
    39 |         Command::Import { keyring_files } => import::run(&config, keyring_files.to_owned()),
       |                                                          ^^^^^^^ help: change this to: `config`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
       = note: `#[warn(clippy::needless_borrow)]` on by default
2025-05-04 11:02:02 +02:00
Zeke Fast
c901336b12 Format code. 2025-05-04 11:02:02 +02:00
Zeke Fast
1442bed329 Rename command handler from module_name::do_something pattern to module_name::run.
I think it looks a bit weird to have things like module "import" with function
"do_import" which is a command handler. So, I renamed all such command
handlers to simple "run" functions. IMO on call side it looks now
better, e.g.: import::run().

Changes:
- Rename the following command handler functions:
  - hagridctl::import::do_import -> hagridctl::import::run
  - hagrid::delete::delete:delete -> hagrid::delete::delete::run
    (still weird, but minus one delete. First delete in path is bin
     name, second - module for command handler)
  - tester::generate::do_generate -> tester::generate::run
  - tester::genreqs::do_genreqs -> tester::genreqs::run
2025-05-04 11:02:01 +02:00
Zeke Fast
5757ac2819 Migrate "hagrid-delete" from StructOpt to Clap v4 derive based API. Clean up code along the way.
Changes:
- Add "derive" and "unicode" features for "clap" in Cargo.toml.
- Remove dependency on "structopt" crate from Cargo.toml.
- Update Cargo.lock.
- Rename crate::delete::cli::Opt struct to Cli.
- Replace usage of #[structopt()] macros with #[derive(Parser)].
- Rename variables and function parameters from "opt" to "cli"
  accordingly.
- Don't parse opt.query in dispatch_cmd function. As Opt.query field
  type was replaced from String to Query in Cli.query new version of
  Clap automatically determines value_parser and calls FromStr::from_str
  implementation to convert str slice into field of type Query.
- Replace improrts of structopt::StructOpt with clap::Parser.
2025-05-04 11:02:01 +02:00
Zeke Fast
0027a25486 Format code. 2025-05-04 11:02:01 +02:00
Zeke Fast
012be246d1 Migrate "hagridctl" to Clap v4 derive based API. Clean up code along the way.
Changes:
- Add "derive" feature for "clap" in hagridctl/Cargo.toml.
- Move about description from hagridctl/src/cli.rs to "description" field in
  hagridctl/Cargo.toml. So, "clap" can access it via #[command(about)]
  macro configuration.
- Update Cargo.lock.
- Replace in hagridctl/src/cli.rs usage of clap::App from Clap 2.34.0
  with clap::Parser derive macro based API, cli::Cli struct and cli::Command
  enum.
- Omit explicit specification of args name which resulted in upper
  casing cmd option's parameters, e.g. "<keyring files>" -> "<KEYRING_FILES>".
- Replace in cli::dispatch_cmd() "if let" based logic with simple match
  on cli::Command enum variants.
- Get rid of manual parsing of options' and commands' values in
  cli::dispatch_cmd(). As it is hadled by derived
  value_parser (https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes)
  based on field types.
- Replace set of limited values specified with .possible_values() method
  call with #[arg(value_enum)] macro and hagridctl::cli::DeploymentEnvironment
  enum. I make environment names fully qualified, but left abbreviations
  as aliases, so `just hagridctl dev import` kind of commands still
  allowed.
- Replace in hagridctl/src/main.rs cli::app().get_matches() (which is Clap
  v2 API) with cli::Cli::parse() (Clap v4 API).
- Add "unicode" feature flag for clap dependency in hagridctl/Cargo.toml.
- Refactor config::load() and to use cli::Cli struct instead of
  clap::ArgMatches. Defaulting to Rocket.toml for configuration file was
  moved to default value for hagridctl::cli::Cli.env field.
- Replace .unwrap() calls in config::load() function with ? operator.
- Extract match statement from config::load() to
  HagridConfigs::get_by(deployment_environment) method.
2025-05-04 11:00:53 +02:00
Zeke Fast
48ef918d51 Migrate "hagrid-tester" to Clap v4 derive based API. Clean up code along the way.
Changes:
- Add "derive" feature for "clap" in tester/Cargo.toml.
- Move about description from tester/src/cli.rs to "description" field in
  tester/Cargo.toml. So, "clap" can access it via #[command(about)]
  macro configuration.
- Update Cargo.lock.
- Replace in tester/src/cli.rs usage of clap::App from Clap 2.34.0
  with clap::Parser derive macro based API, cli::Cli struct and cli::Command
  enum.
- Remove "--config/-c <FILE>" option for "tester" as it wasn't used and
  I believe it was a copy-paste from "hagridctl".
- Omit explicit specification of args name which resulted in upper
  casing cmd option's parameters, e.g. "<cert count>" -> "<CERT_COUNT>".
- Replace in cli::dispatch_cmd() "if let" based logic with simple match
  on cli::Command enum variants.
- Get rid of manual parsing of options' and commands' values in
  cli::dispatch_cmd(). As it is hadled by derived
  value_parser (https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes)
  based on field types.
- Make "fprs_path" parameter of tester::generate::do_generate function
  obligatory (previously it was Option<&Path>) as this argument has a
  default value to "fingerprints.txt" which is populated by clap. So, it
  never be None.
  In addition, clean up logic related to optional "fprs_path" parameter
  in tester::generate::do_generate function.
- Change signatures of the following functions to accept
  "impl AsRef<Path>" instead of &Path or Option<&Path>:
  - tester::generate::do_generate()
  - tester::genreqs::do_genreqs()
  That allows us to pass as function arguments anything from which Path
  can be borrowed, e.g. PathBuf without the need to explicitly transform
  the value at call site with calls like .as_path() or .as_deref().
- Replace in tester/src/main.rs cli::app().get_matches() (which is Clap
  v2 API) with cli::Cli::parse() (Clap v4 API).
2025-05-04 10:58:49 +02:00
Zeke Fast
333727a6dc Bump "clap" dependency version in Cargo.toml: "2" -> ">= 4.5.37".
Commands:

    cargo update clap@2.34.0

Additional Changes:
- Update Cargo.lock.
2025-05-04 10:58:49 +02:00
Zeke Fast
a7186bb6df Extract error printing logic from src/delete/main.rs to cli::print_errors function.
Changes:
- Extract error printing logic from src/delete/main.rs to cli::print_errors function.
- Move return error code from magic constant to crate::delete:ERROR_EXIT_CODE constant.
- Rename crate::delete::real_main() to crate::delete::run().
2025-05-04 10:58:48 +02:00
Zeke Fast
83b0515274 Extract "delete" action from src/delete/main.rs to its own module ("delete").
This is to better separate CLI handling, wiring and action logic.
2025-05-04 10:58:48 +02:00
Zeke Fast
b296157c08 Extract cmd dispatch code from src/delete/main.rs to delete::cli module.
Changes:
- Introduce cli::dispatch_cmd() function which just delegate to delete function.
2025-05-04 10:58:48 +02:00
Zeke Fast
abeafbe3d4 Move option parsing logic from src/delete/main.rs to newly introduced delete::cli module.
Changes:
- Introduce "delete::cli" module in "hagrid" crate.
- Move option parsing logic there from src/delete/main.rs (former src/delete.rs).
2025-05-04 10:58:47 +02:00
Zeke Fast
d35136b0d6 Promote src/delete.rs module to directory in "hagrid" crate.
Additional Changes:
- Change "hagrid-delete" bin declaration in Cargo.toml accordingly.
2025-05-04 10:58:47 +02:00
Zeke Fast
9acb4b52db Extract config related code in "hagridctl" crate from main.rs to "config" module.
Changes:
- Move the following structs from main.rs to "config" module:
  - HagridConfigs
  - HagridConfig
- Introduce "config::load" function and move configuration loading code into it.
- Adjust imports accordingly.
- Share ArgMatches returned by App::get_matches between
  config::load and cli::dispatch_cmd functions.
- Make HagridConfigs struct private to the config module as rest of the
  system doesn't need to know about it and interacts only with
  HagridConfig struct.
2025-05-04 10:58:47 +02:00
Zeke Fast
c0c8247c42 Extract CLI UI code in "hagridctl" crate from main.rs to "cli" module.
Additional Changes:
- Split "app" construction and command dispatch phases into different
  functions, hence app() and dispatch_cmd() functions.
2025-05-04 10:58:11 +02:00
Zeke Fast
f8982939aa Extract CLI UI code in "tester" crate from main.rs to "cli" module.
Additional Changes:
- Split "app" construction and command dispatch phases into different
  functions, hence app() and dispatch_cmd() functions.
2025-05-03 11:06:05 +02:00
Zeke Fast
b66fb67302 Add "just" recipes and aliases for runing bin-artifacts.
This allows for much more convenient cmd binary launching without the need
to remember distinction bitween binaries in main crate (hagrid) and
others (hagridctl, tester) and need to provide additional -- to cargo to
pass arguments to binaries.

Apart from convenience it also provide kinda "documentation" of binary
artifacts in the project as "run" group in just collect all the
artifacts together.

Changes:
- Add new named aliases for `just run` which runs "hagrid" binary with web
  server:
  - run-hagrid
  - hagrid
- Add new recipe to run "hagrid-delete" binary "run-hagrid-delete".
- Alias "run-hagrid-delete" recipe as
  - hagrid-delete
  - delete
- Add new recipe to run "hagridctl" crate with default binary
  "run-hagridctl".
- Alias "run-hagridctl" recipe as
  - hagridctl
- Add new recipe to run "tester" crate with default binary "run-tester".
- Alias "run-tester" recipe as
  - tester
2025-05-03 09:43:00 +02:00
Zeke Fast
84cfb5afaf Move development dependencies documentation from DEPENDENCIES.md to README.md.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/213#note_2481434850
2025-05-02 16:04:52 +02:00
Zeke Fast
e966c1fbb7 Remove not used file contrib/hagrid-daily-backup.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/213#note_2481434830
2025-05-02 16:04:52 +02:00
Zeke Fast
70b0eeb3e7 Introduce DEPENDENCIES.md file with list of software help for development. Reference DEPENDENCIES.md from README.md.
Changes:
- Document software dependencies for development process in
  DEPENDENCIES.md. file.
- Reference DEPENDENCIES.md from README.md.
2025-05-02 16:04:51 +02:00
Zeke Fast
7156d92246 Introduce justfile with bunch of recipes to streamline project development.
To install `just` follow instructions in
documentation https://just.systems/man/en/packages.html .

Full list of recipes and their description can be viewed by running
`just` command in project's root directory.

But here is the list of added commands for documentation sake:
- init                    # Perform initial setup of developer's system.
- init-rocket-config      # Copy Rocket's template configuration from Rocket.toml.disk to Rocket.toml. Rocket is Rust web framework. See https://rocket.rs/guide/v0.5/configuration/#configuration
- just-fmt                # Format justfile
- cargo-fmt               # Format Rust code in all packages (aka path based dependencies)
- fmt                     # Format all code [alias: f]
- just-lint-fmt           # Check justfile formatting
- cargo-lint-fmt          # Check Rust code formatting in all packages (aka path based dependencies)
- lint-fmt                # Check formatting of all code [alias: lf]
- clippy-lint             # Lint Rust code with Clippy [alias: cl]
- lint                    # Lint all code [alias: l]
- clippy-fix *args        # Apply Clippy's lint suggestions, i.e. fix Clippy linting warnings or errors
- cargo-fix *args         # Fix compilation warnings by applying compiler suggestions
- fix *args               # Fix lint and compilation warnings and errors. Pass given arguments to all sub-recipes, i.e. `just fix --allow-dirty` calls `just cargo-fix --allow-dirty` and `just clippy-fix --allow-dirty`.
- check                   # Check Rust code errors [alias: c]
- build                   # Compile Rust code [alias: b]
- test args='--workspace' # Run all tests (i.e. --workspace), but when args given pass them to `cargo test`, e.g. `just test fs::tests::init` [alias: t]
- run                     # Run web server [alias: r]
- watch-check *args       # Run continuous check of Rust code errors. Detect file changes and repeat check automatically. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N). [alias: wc]
- watch-run *args         # Run web server and automatically restart on changes. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N). [alias: wr]
- watch-test *args        # Run tests every time files changed. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N). [alias: wt]
- clean                   # Clean compilartion artifacts (i.e. "target" directory)
- clean-translations      # Clean changes to translation files
- translate-templates     # Translate *.hbs templates of web pages
- db                      # Open database prompt
2025-05-02 16:04:51 +02:00
Zeke Fast
893f99c460 Improve error reporting in Zsh scripts by providing -euo pipefail options.
Documentation: https://linux.die.net/man/1/zshoptions
-e Exit on non-zero return code from commands
-u NO_UNSET
-o pipefail propagates piping errors
2025-05-01 00:24:11 +02:00
Zeke Fast
90d00637a0 Replace usage of deprecated cmd option "--all" with "--workspace" for "cargo test" command in .gitlab-ci.yml file.
`cargo help test` on 1.86.0 version says:

>       --all
>           Deprecated alias for --workspace.
2025-05-01 00:19:46 +02:00
Zeke Fast
3a3aba5db1 Replace redundant match expression with .unwrap_or_else. 2025-04-30 09:25:06 +02:00
Zeke Fast
0a829824dc Eliminate redundant conversions to Option and usage of filter_map. Use flat_map directly instead.
Documentation:
- https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map
- https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flat_map

Additional Changes:
- Use point-less style and pass Email::try_from function directly to
  flat_map.
  flat_map passes uid to Email::try_from implicitly. That's why it's
  called point-less style. IMO it looks nicer!
2025-04-30 09:25:06 +02:00
Zeke Fast
51a66d643d Get rid of aliasing of "hagrid_database" as "database".
It improves code readability and consistency. In addition, less names
used in the code base.
2025-04-30 09:25:06 +02:00
Zeke Fast
2059c69226 Get rid of aliasing of "sequoia_openpgp" as "openpgp".
It improves code readability and consistency. In addition, less names
used in the code base.
2025-04-30 09:25:06 +02:00
Zeke Fast
0bea4f0f2a Remove imports of anyhow::Error and use named path anyhow::Error in place.
This is done for the same reason as for anyhow::Result and
core::result::Result types.

Error is whidely used name and IMO it usually good to keep it standard.
Apart from that I think assosicated type specification looks fore
readable when anyhow::Error is used in place. For example:

    type Error = Error;

was replaced with

    type Error = anyhow::Error;

Changes:
- Replace usage of imported unquolified Error type with anyhow::Error.
- Remove imports of anyhow::Error.
2025-04-30 09:25:05 +02:00
Zeke Fast
4d57dc1eb2 Replace imported anyhow::Result with usage of anyhow::Result in place.
This is to make this few files to much in style rest of code base.
2025-04-30 09:25:05 +02:00
Zeke Fast
0cfd412907 Clean up linting warnings about unused imports.
warning: unused import: `std::result`
     --> database/src/types.rs:3:5
      |
    3 | use std::result;
      |     ^^^^^^^^^^^
      |
      = note: `#[warn(unused_imports)]` on by default
2025-04-30 09:25:05 +02:00
Zeke Fast
9094b09b27 Format code. 2025-04-30 09:25:05 +02:00
Zeke Fast
c28e6af441 Restore default Result type to std::result::Result (aka core::result::Result).
Result it widely used type and the name is highly overloaded as almost
every library tend to define they own Result type.
When default Result type deviates from default it is usually harder to
read code and requires a bit of investigation work to understand this
particular Result corresponds to.

In Hagrid code base in many places anyhow::Result was made a default
type. It usually was imported through indirection, i.e.

    use anyhow::Result;

was imported in files like

- database/src/lib.rs
- hagridctl/src/import.rs
- hagridctl/src/main.rs
- src/main.rs

and in submodules that above import was reimported with something like

    use crate::Result;

or

    use super::Result;

So, I qualified such Result as anyhow::Result which IMO make code easier
to understand and "anyhow" is short enough name to use along with
Result.
All imports and reimports was cleaned up.

Changes:
- Full qualify anyhow's Result name as anyhow::Result in places where it
  is used to make code easier to read and understand.
- Remove imports and reimports of anyhow::Result.
- Restore default Result type to core::result::Result (aka std::result::Result).
- Clean up unnecesary name path qualification for Result type
  like "std::result" or "result" as it becomes a default one and doesn't
  overriden any more.
2025-04-30 09:25:04 +02:00
Zeke Fast
37e6b2cb09 Remove "lazy_static" crate dependency from Cargo.toml.
Additional Changes:
- Update Cargo.lock.
2025-04-29 23:28:30 +02:00
Zeke Fast
fefebaaffe Replace usage of lazy_static! macro with std::sync::LazyLock.
Now as we on new version of Rust (1.86.0) we can benefit from fairly
recent addition to std, like LazyLock. Nice benefit is one less crate to
depend on.

Documentation:
- https://crates.io/crates/lazy-static
- https://blog.rust-lang.org/2024/07/25/Rust-1.80.0/#lazycell-and-lazylock
- https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html

Not particularly useful for code changes, but might be helpful for thus
who want to navigate these Once, Lazy, Cell, Lock types and figure out
differences between them.

Additional Documentation:
- https://crates.io/crates/once_cell
- https://blog.rust-lang.org/2023/06/01/Rust-1.70.0/#oncecell-and-oncelock
- https://doc.rust-lang.org/stable/core/cell/index.html
- https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html

Changes:
- Move the following statics out of lazy_static! macro and initialize
  them using std::sync::LazyLock:
  - POPULAR_DOMAINS
  - KEY_UPLOAD
  - MAIL_SENT
  - KEY_ADDRESS_PUBLISHED
  - KEY_ADDRESS_UNPUBLISHED
- Remove lazy_static imports.
2025-04-29 23:28:30 +02:00
Zeke Fast
9bc3ccecac Format code (mostly imports).
Commands:

    cargo fmt --all
2025-04-28 00:38:39 +02:00
Zeke Fast
09072200d6 Replace "extern crate" and #[macro_use] declarations with "use" imports.
Both "extern crate" and #[macro_use] are artifacts of earlier editions
of Rust language. Nowadays we can entirely rely on "use" instead.

Changes:
- Replace "extern crate" with "use" imports.
- Replace

    #[macro_use]
    extern crate ...;

  declarations with "use" imports of used macros. For example,

    #[macro_use]
    extern crate anyhow;

  was replaced with

    use anyhow::anyhow;

  in every file where anyhow! macro were used.
- Favor direct usage of import path instead of aliased one.
  For example, in many places "sequoia_opengpg" were aliased as "openpgp",
  during imports replacements I tried to avoid usage of "openpgp" or
  introduced additional aliases (like "use sequoia_openpgp as openpgp")
  and used "sequoia_opengpg".
  I think this way it is easier to understand where name came from
  instead of search and jumping to lib.rs or main.rs files trying to
  find where name were aliased.
  Another example of such favoring is usage of "hagrid_database" over
  the "database" in imports.
  NOTE: the usage is still inconsistent and requires further clean up.
2025-04-27 23:36:59 +02:00
Zeke Fast
5399e6c2d3 Update CI image in .gitlab-ci.yml: "rust:1-bullseye" -> "rust:1.86-bookworm". 2025-04-27 18:48:29 +02:00
Zeke Fast
77372abb7c Add "rustfmt" and "clippy" to components in rust-toolchain.toml file.
Documentation:
- https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file
- https://rust-lang.github.io/rustup/concepts/components.html
2025-04-27 18:31:58 +02:00
Zeke Fast
0200c15266 Format code.
Commands:

    cargo fmt --all
2025-04-27 18:12:57 +02:00
Zeke Fast
e4aac748be Clean up linting warnings: this manual char comparison can be written more succinctly.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: this manual char comparison can be written more succinctly
       --> src/web/vks_api.rs:117:37
        |
    117 |         .flat_map(|lang| lang.split(|c| c == '-' || c == ';' || c == '_').next())
        |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using an array of `char`: `['-', ';', '_']`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_pattern_char_comparison
        = note: `#[warn(clippy::manual_pattern_char_comparison)]` on by default
2025-04-27 18:04:41 +02:00
Zeke Fast
8b6049cb45 Clean up linting warnings: manual implementation of split_once.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: manual implementation of `split_once`
       --> src/mail.rs:340:17
        |
    340 |                 let mut it = line.splitn(2, ": ");
        |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    341 |                 let h = it.next().unwrap();
        |                 --------------------------- first usage here
    342 |                 let v = it.next().unwrap();
        |                 --------------------------- second usage here
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_split_once
        = note: `#[warn(clippy::manual_split_once)]` on by default
    help: replace with `split_once`
        |
    340 ~                 let (h, v) = line.split_once(": ").unwrap();
    341 ~
    342 ~
        |
2025-04-27 17:53:56 +02:00
Zeke Fast
45402ddd07 Clean up linting warnings: unnecessary closure used with `bool::then.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: unnecessary closure used with `bool::then`
       --> src/dump.rs:125:21
        |
    125 |                     pp.decrypt(algo, &sk.session_key).is_ok().then(|| algo)
        |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
        = note: `#[warn(clippy::unnecessary_lazy_evaluations)]` on by default
    help: use `then_some` instead
        |
    125 -                     pp.decrypt(algo, &sk.session_key).is_ok().then(|| algo)
    125 +                     pp.decrypt(algo, &sk.session_key).is_ok().then_some(algo)
        |
2025-04-27 17:46:04 +02:00
Zeke Fast
e85e414619 Clean up linting warnings: allocating a new String only to create a temporary &str from it.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: allocating a new `String` only to create a temporary `&str` from it
       --> database/src/test.rs:109:37
        |
    109 |         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
        |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_to_owned
        = note: `#[warn(clippy::unnecessary_to_owned)]` on by default
    help: convert from `&[u8]` to `&str` directly
        |
    109 -         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
    109 +         let email = Email::from_str(core::str::from_utf8(uid.value()).unwrap()).unwrap();
        |

    warning: allocating a new `String` only to create a temporary `&str` from it
       --> database/src/test.rs:137:37
        |
    137 |         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
        |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_to_owned
    help: convert from `&[u8]` to `&str` directly
        |
    137 -         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
    137 +         let email = Email::from_str(core::str::from_utf8(uid.value()).unwrap()).unwrap();
        |
2025-04-27 17:36:39 +02:00
Zeke Fast
fcc9689ef3 Clean up linting warnings: manually reimplementing div_ceil.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: manually reimplementing `div_ceil`
      --> hagridctl/src/import.rs:58:22
       |
    58 |     let chunk_size = (input_files.len() + (num_threads - 1)) / num_threads;
       |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using `.div_ceil()`: `input_files.len().div_ceil(num_threads)`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_div_ceil
       = note: `#[warn(clippy::manual_div_ceil)]` on by default
2025-04-27 17:26:52 +02:00
Zeke Fast
896206d6ca Clean up linting warnings: this if has identical blocks.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: this `if` has identical blocks
      --> database/src/openpgp_utils.rs:89:27
       |
    89 |           if !is_exportable {
       |  ___________________________^
    90 | |             false
    91 | |         } else if is_status_revoked(uid.revocation_status(&POLICY, None)) {
       | |_________^
       |
    note: same as this
      --> database/src/openpgp_utils.rs:91:75
       |
    91 |           } else if is_status_revoked(uid.revocation_status(&POLICY, None)) {
       |  ___________________________________________________________________________^
    92 | |             false
    93 | |         } else if let Ok(email) = Email::try_from(uid.userid()) {
       | |_________^
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#if_same_then_else
       = note: `#[warn(clippy::if_same_then_else)]` on by default
2025-04-27 17:11:24 +02:00
Zeke Fast
f4699a4545 Clean up linting warnings: accessing first element with index 0.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: accessing first element with `self.catalogs.get(0)`
      --> src/i18n.rs:21:32
       |
    21 |             .unwrap_or_else(|| self.catalogs.get(0).unwrap());
       |                                ^^^^^^^^^^^^^^^^^^^^ help: try: `self.catalogs.first()`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#get_first
       = note: `#[warn(clippy::get_first)]` on by default

    warning: accessing first element with `tpk_status
                     .email_status.get(0)`
       --> src/web/vks.rs:364:23
        |
    364 |       let primary_uid = tpk_status
        |  _______________________^
    365 | |         .email_status
    366 | |         .get(0)
        | |_______________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#get_first
    help: try
        |
    364 ~     let primary_uid = tpk_status
    365 +         .email_status.first()
        |
2025-04-27 16:59:14 +02:00
Zeke Fast
57a8e3a3a8 Clean up linting warnings: length comparison to zero.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: length comparison to zero
       --> database/src/sqlite.rs:518:17
        |
    518 |         assert!(db.merge(k1).unwrap().into_tpk_status().email_status.len() > 0);
        |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!db.merge(k1).unwrap().into_tpk_status().email_status.is_empty()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
        = note: `#[warn(clippy::len_zero)]` on by default

    warning: length comparison to zero
       --> database/src/sqlite.rs:520:13
        |
    520 | /             db.merge(k2.clone())
    521 | |                 .unwrap()
    522 | |                 .into_tpk_status()
    523 | |                 .email_status
    524 | |                 .len()
    525 | |                 > 0
        | |___________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
    help: using `!is_empty` is clearer and more explicit
        |
    520 ~             !db.merge(k2.clone())
    521 +                 .unwrap()
    522 +                 .into_tpk_status()
    523 +                 .email_status.is_empty()
        |

    warning: length comparison to zero
       --> database/src/sqlite.rs:529:13
        |
    529 | /             db.merge(k3.clone())
    530 | |                 .unwrap()
    531 | |                 .into_tpk_status()
    532 | |                 .email_status
    533 | |                 .len()
    534 | |                 > 0
        | |___________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
    help: using `!is_empty` is clearer and more explicit
        |
    529 ~             !db.merge(k3.clone())
    530 +                 .unwrap()
    531 +                 .into_tpk_status()
    532 +                 .email_status.is_empty()
        |

    warning: length comparison to zero
       --> src/dump.rs:999:24
        |
    999 |                     if reason.len() > 0 { ", " } else { "" },
        |                        ^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!reason.is_empty()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
        = note: `#[warn(clippy::len_zero)]` on by default
2025-04-27 16:51:28 +02:00
Zeke Fast
1f67668500 Clean up linting warnings: redundant return statements.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: unneeded `return` statement
       --> database/src/sqlite.rs:293:18
        |
    293 |             _ => return None,
        |                  ^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
        = note: `#[warn(clippy::needless_return)]` on by default
    help: remove `return`
        |
    293 -             _ => return None,
    293 +             _ => None,
        |

    warning: unneeded `()`
       --> hagridctl/src/import.rs:153:25
        |
    153 |             _ => return (),
        |                         ^^ help: remove the `()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unused_unit
        = note: `#[warn(clippy::unused_unit)]` on by default

    warning: unneeded `()`
       --> hagridctl/src/import.rs:176:20
        |
    176 |             return ();
        |                    ^^ help: remove the `()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unused_unit

    warning: unneeded `return` statement
       --> src/web/hkp.rs:169:5
        |
    169 | /     return format!(
    170 | |         "Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload",
    171 | |         baseuri = origin.get_base_uri()
    172 | |     );
        | |_____^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
        = note: `#[warn(clippy::needless_return)]` on by default
    help: remove `return`
        |
    169 ~     format!(
    170 +         "Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload",
    171 +         baseuri = origin.get_base_uri()
    172 ~     )
        |
2025-04-27 16:22:10 +02:00
Zeke Fast
27b68dc826 Clean up linting warnings: related to iterators, mostly replacement of flatten() with filter_map() or flat_map().
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
       --> database/src/fs.rs:614:14
        |
    614 |             .last()
        |              ^^^^^^ help: try: `next_back()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#double_ended_iterator_last
        = note: `#[warn(clippy::double_ended_iterator_last)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/fs.rs:708:18
        |
    708 |                   .map(Fingerprint::try_from)
        |  __________________^
    709 | |                 .flatten();
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(Fingerprint::try_from)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
        = note: `#[warn(clippy::map_flatten)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:424:18
        |
    424 |                   .map(|email| Email::from_str(email))
        |  __________________^
    425 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|email| Email::from_str(email))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:422:18
        |
    422 |                   .map(|uid| uid.userid().email2().unwrap())
        |  __________________^
    423 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `filter_map` and remove the `.flatten()`: `filter_map(|uid| uid.userid().email2().unwrap())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:431:18
        |
    431 |                   .map(|email| Email::from_str(&email.unwrap()))
        |  __________________^
    432 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|email| Email::from_str(&email.unwrap()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:453:18
        |
    453 |                   .map(Fingerprint::try_from)
        |  __________________^
    454 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(Fingerprint::try_from)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:266:14
        |
    266 |               .map(|binding| {
        |  ______________^
    267 | |                 if let Ok(email) = Email::try_from(binding.userid()) {
    268 | |                     Some((binding, email))
    269 | |                 } else {
    ...   |
    272 | |             })
    273 | |             .flatten()
        | |______________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
    help: try replacing `map` with `filter_map` and remove the `.flatten()`
        |
    266 ~             .filter_map(|binding| {
    267 +                 if let Ok(email) = Email::try_from(binding.userid()) {
    268 +                     Some((binding, email))
    269 +                 } else {
    270 +                     None
    271 +                 }
    272 +             })
        |

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:315:22
        |
    315 |                       .map(|uid| Email::try_from(uid).ok())
        |  ______________________^
    316 | |                     .flatten()
        | |______________________________^ help: try replacing `map` with `filter_map` and remove the `.flatten()`: `filter_map(|uid| Email::try_from(uid).ok())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:492:14
        |
    492 |               .map(|uid| Email::try_from(uid).ok())
        |  ______________^
    493 | |             .flatten()
        | |______________________^ help: try replacing `map` with `filter_map` and remove the `.flatten()`: `filter_map(|uid| Email::try_from(uid).ok())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:509:14
        |
    509 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    510 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:582:14
        |
    582 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    583 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:590:14
        |
    590 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    591 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:650:14
        |
    650 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    651 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:708:10
        |
    708 |           .map(|binding| Email::try_from(binding.userid()))
        |  __________^
    709 | |         .flatten()
        | |__________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: useless conversion to the same type: `openpgp::cert::prelude::KeyAmalgamationIter<'_, openpgp::packet:🔑:PublicParts, openpgp::packet:🔑:UnspecifiedRole>`
       --> database/src/lib.rs:716:5
        |
    716 | /     tpk.keys()
    717 | |         .into_iter()
        | |____________________^ help: consider removing `.into_iter()`: `tpk.keys()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
        = note: `#[warn(clippy::useless_conversion)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
      --> src/web/manage.rs:78:22
       |
    78 |                       .map(|u| u.userid().to_string().parse::<Email>())
       |  ______________________^
    79 | |                     .flatten()
       | |______________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|u| u.userid().to_string().parse::<Email>())`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
       = note: `#[warn(clippy::map_flatten)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
       --> src/web/vks.rs:225:10
        |
    225 |           .map(|address| address.parse::<Email>())
        |  __________^
    226 | |         .flatten()
        | |__________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|address| address.parse::<Email>())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
2025-04-27 16:09:54 +02:00
Zeke Fast
535668c507 Clean up linting warnings: replace usage of explicit lifetimes with elision.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: the following explicit lifetimes could be elided: 'a
       --> database/src/fs.rs:348:6
        |
    348 | impl<'a> FilesystemTransaction<'a> {
        |      ^^                        ^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
        = note: `#[warn(clippy::needless_lifetimes)]` on by default
    help: elide the lifetimes
        |
    348 - impl<'a> FilesystemTransaction<'a> {
    348 + impl FilesystemTransaction<'_> {
        |

    warning: the following explicit lifetimes could be elided: 'a
       --> database/src/sqlite.rs:141:6
        |
    141 | impl<'a> DatabaseTransaction<'a> for SqliteTransaction {
        |      ^^                      ^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
    help: elide the lifetimes
        |
    141 - impl<'a> DatabaseTransaction<'a> for SqliteTransaction {
    141 + impl DatabaseTransaction<'_> for SqliteTransaction {
        |

    warning: the following explicit lifetimes could be elided: 'a
      --> src/dump.rs:40:6
       |
    40 | impl<'a> std::fmt::Display for SessionKeyDisplay<'a> {
       |      ^^                                          ^^
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
       = note: `#[warn(clippy::needless_lifetimes)]` on by default
    help: elide the lifetimes
       |
    40 - impl<'a> std::fmt::Display for SessionKeyDisplay<'a> {
    40 + impl std::fmt::Display for SessionKeyDisplay<'_> {
       |
2025-04-27 15:09:23 +02:00
Zeke Fast
c77bf9d3db Clean up linting warnings: deref coersion related.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: called `.as_ref().map(|f| f.as_path())` on an `Option` value
      --> tester/src/main.rs:78:13
       |
    78 |             output_fprs.as_ref().map(|f| f.as_path()),
       |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using as_deref: `output_fprs.as_deref()`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref
       = note: `#[warn(clippy::option_as_ref_deref)]` on by default

    warning: deref which would be done by auto-deref
       --> src/web/vks_web.rs:358:52
        |
    358 |     for ValueField { name, value } in Form::values(&*String::from_utf8_lossy(&buf)) {
        |                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `&String::from_utf8_lossy(&buf)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#explicit_auto_deref
        = note: `#[warn(clippy::explicit_auto_deref)]` on by default
2025-04-27 15:01:26 +02:00
Zeke Fast
fec6763b75 Clean up linting warnings: remove unnecessary borrowings.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:258:40
        |
    258 |         let typ = fs::symlink_metadata(&path).ok()?.file_type();
        |                                        ^^^^^ help: change this to: `path`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args
        = note: `#[warn(clippy::needless_borrows_for_generic_args)]` on by default

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:287:44
        |
    287 |             let typ = fs::symlink_metadata(&path)?.file_type();
        |                                            ^^^^^ help: change this to: `path`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:328:13
        |
    328 |     symlink(&symlink_content, &symlink_name_tmp)?;
        |             ^^^^^^^^^^^^^^^^ help: change this to: `symlink_content`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:329:31
        |
    329 |     rename(&symlink_name_tmp, &symlink_name)?;
        |                               ^^^^^^^^^^^^^ help: change this to: `symlink_name`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:334:35
        |
    334 |     if let Ok(target) = read_link(&link) {
        |                                   ^^^^^ help: change this to: `link`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> database/src/sqlite.rs:424:46
        |
    424 |                 .map(|email| Email::from_str(&email))
        |                                              ^^^^^^ help: change this to: `email`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
        = note: `#[warn(clippy::needless_borrow)]` on by default

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> database/src/lib.rs:546:51
        |
    546 |                 self.set_email_unpublished_filter(&tx, &current_fpr, |uid| {
        |                                                   ^^^ help: change this to: `tx`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> database/src/lib.rs:602:29
        |
    602 |         self.regenerate_wkd(&tx, fpr_primary, &published_tpk_clean)?;
        |                             ^^^ help: change this to: `tx`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> hagridctl/src/import.rs:161:86
        |
    161 |                         db.set_email_published(&key_fpr.clone().try_into().unwrap(), &email)
        |                                                                                      ^^^^^^ help: change this to: `email`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
        = note: `#[warn(clippy::needless_borrow)]` on by default

    warning: this expression creates a reference which is immediately dereferenced by the compiler
      --> src/sealed_state.rs:20:37
       |
    20 |         let cipher = Aes256Gcm::new(&key);
       |                                     ^^^^ help: change this to: `key`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
       = note: `#[warn(clippy::needless_borrow)]` on by default

    warning: the borrowed expression implements the required traits
      --> src/template_helpers.rs:46:77
       |
    46 |                         remove_extension(remove_extension(path.strip_prefix(&template_path)?));
       |                                                                             ^^^^^^^^^^^^^^ help: change this to: `template_path`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args
       = note: `#[warn(clippy::needless_borrows_for_generic_args)]` on by default

    warning: the borrowed expression implements the required traits
       --> src/web/vks.rs:216:46
        |
    216 |         Err(e) => return UploadResponse::err(&e.to_string()),
        |                                              ^^^^^^^^^^^^^^ help: change this to: `e.to_string()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> src/web/vks.rs:248:40
        |
    248 |             return UploadResponse::err(&format!("error sending email to {}", &email));
        |                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: change this to: `format!("error sending email to {}", &email)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args
2025-04-27 15:01:12 +02:00
Zeke Fast
80df057617 Clean up linting warnings: lines_filter_map_ok.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: `flatten()` will run forever if the iterator repeatedly produces an `Err`
      --> tester/src/genreqs.rs:12:70
       |
    12 |     let fingerprints: Vec<String> = io::BufReader::new(file).lines().flatten().collect();
       |                                                                      ^^^^^^^^^ help: replace with: `map_while(Result::ok)`
       |
    note: this expression returning a `std::io::Lines` may produce an infinite number of `Err` in case of a read error
      --> tester/src/genreqs.rs:12:37
       |
    12 |     let fingerprints: Vec<String> = io::BufReader::new(file).lines().flatten().collect();
       |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok
       = note: `#[warn(clippy::lines_filter_map_ok)]` on by default
2025-04-27 15:00:50 +02:00
Zeke Fast
5731f8aa2b Clean up linting warnings: redundant import.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: this import is redundant
      --> database/src/fs.rs:12:1
       |
    12 | use tempfile;
       | ^^^^^^^^^^^^^ help: remove it entirely
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_component_path_imports
       = note: `#[warn(clippy::single_component_path_imports)]` on by default
2025-04-27 15:00:16 +02:00
Zeke Fast
62b936864d Run cargo fmt --all to format code to pass CI checks.
Commands:

    cargo fmt --all
2025-04-27 13:37:18 +02:00
Zeke Fast
4f86585ac3 Resolve dependency version differences for "sequoia-openpgp" crate.
It eliminates the following output of `cargo autoinherit` command:

    `sequoia-openpgp` won't be auto-inherited because there are multiple sources for it:
      - version: =1.17.0
      - version: ^1.17.0

Changes:
- Allow "sequoia-openpgp" crate to use workspace dependencies by resolving
  version differences constraints in Cargo.toml files.
2025-04-27 13:20:00 +02:00
Zeke Fast
8db33156c3 Resolve dependency version differences for "multipart" crate.
It eliminates the following output of `cargo autoinherit` command:

    `multipart` won't be auto-inherited because there are multiple sources for it:
      - version: ^0
      - version: ~0.18

Changes:
- Allow "multipart" crate to use workspace dependencies by resolving
  version differences constraints in Cargo.toml files.
2025-04-27 13:19:59 +02:00
Zeke Fast
b8fdaeb3c6 Unify crates dependency management in project's Cargo.toml using workspace dependencies.
This simplifies dependencies management and upgrades while ensuring that
dependencies version aligned with all the crates in the project and
neither dependency is used twice with different versions by accident
(though dependencies still can appear several times as sub-dependencies due to
 misaligned version constraints for dependency resolution).

Documentation and useful articles:
- https://mainmatter.com/blog/2024/03/18/cargo-autoinherit/
- https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
- https://crates.io/crates/cargo-autoinherit

Commands:

    `cargo autoinherit`

Output:

    $ cargo autoinherit
    `multipart` won't be auto-inherited because there are multiple sources for it:
      - version: ^0.18.0
      - version: ^0
    `sequoia-openpgp` won't be auto-inherited because there are multiple sources for it:
      - version: ^1.17.0
      - version: =1.17.0

Changes:
- Collect all the dependencies for workspace's crates in the top level
  Cargo.toml file by applying `cargo autoinherit`.
- Use workspace dependencies in crates Cargo.toml files (i.e.
  crate_name = { workspace = true }).
2025-04-27 13:18:09 +02:00
Zeke Fast
7df4d76d5d Set "resolver" explicitly to "3".
Documentation: https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions

Changes:
- Add explicit specification of "resolver" field to Cargo.toml. Set it
  to version "3".
2025-04-27 11:06:45 +02:00
Zeke Fast
e72c647505 Upgrade set explicit version requirement for "multipart" dependency in Cargo.toml: "0" -> "0.18".
`cargo build` gives the following warnings:

    warning: the following packages contain code that will be rejected by a future version of Rust: buf_redux v0.8.4, multipart v0.18.0, traitobject v0.1.0, typemap v0.3.3
    note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1`

which is when run with `cargo build --future-incompat-report` gives the
following:

    warning: `hagrid` (bin "hagrid") generated 9 warnings
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.91s
    warning: the following packages contain code that will be rejected by a future version of Rust: buf_redux v0.8.4, multipart v0.18.0, traitobject v0.1.0, typemap v0.3.3
    note:
    To solve this problem, you can try the following approaches:

    - Some affected dependencies have newer versions available.
    You may want to consider updating them to a newer version to see if the issue has been fixed.

    traitobject v0.1.0 has the following newer versions available: 0.1.1

    - If the issue is not solved by updating the dependencies, a fix has to be
    implemented by those dependencies. You can help with that by notifying the
    maintainers of this problem (e.g. by creating a bug report) or by proposing a
    fix to the maintainers (e.g. by creating a pull request):

      - buf_redux@0.8.4
      - Repository: https://github.com/abonander/buf_redux
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package buf_redux@0.8.4`

      - multipart@0.18.0
      - Repository: http://github.com/abonander/multipart
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package multipart@0.18.0`

      - traitobject@0.1.0
      - Repository: https://github.com/reem/rust-traitobject.git
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package traitobject@0.1.0`

      - typemap@0.3.3
      - Repository: https://github.com/reem/rust-typemap
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package typemap@0.3.3`

    - If waiting for an upstream fix is not an option, you can use the `[patch]`
    section in `Cargo.toml` to use your own version of the dependency. For more
    information, see:
    https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section

    note: this report can be shown with `cargo report future-incompatibilities --id 1`

In attempt to fix the warning I set explict dependency for "multipart"
crate.

Changes:
- Set explicit version of "multipart" dependency in Cargo.toml: "0" -> "0.18.0".
- Update Cargo.lock: `cargo build`
2025-04-27 11:03:04 +02:00
Zeke Fast
d2ac58b3fa Explicitly set "rust-version" in Cargo.toml. Upgrade Rust version in clippy.toml: 1.58.1 -> 1.86.
Documentation:
- https://github.com/rust-lang/rust-clippy?tab=readme-ov-file#specifying-the-minimum-supported-rust-version
- https://doc.rust-lang.org/clippy/configuration.html#specifying-the-minimum-supported-rust-version
- https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field
- https://doc.rust-lang.org/cargo/reference/rust-version.html

Setting "rust-version" in Cargo.toml might be redundant for Clippy, but
it may serve well for Cargo itself. So, I set it in both places.

Changes:
- Update "msrv" in clippy.toml: 1.58.1 -> 1.86.
- Explicitly set "rust-version" in Cargo.toml to "1.86".
2025-04-27 10:33:06 +02:00
Zeke Fast
c541c19622 Fix imports of test module after switch to 2024 edition.
Changes:
- Add "crate::" to the imports of "test" module in "fs" and "sqlite"
  modules of "database" crate.
2025-04-27 02:04:48 +02:00
Zeke Fast
58959e112e Fix compilation errors with binding modifier "ref" after switch to 2024 edition.
Fix the following and alike errors after switch to 2024 edition:

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/fs.rs:552:27
    |
552 |             ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
    |                           ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/fs.rs:552:13
    |
552 |             ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
    |             ^^^^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
552 -             ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
552 +             ByFingerprint(fp) => self.link_by_fingerprint(fp),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/fs.rs:553:21
    |
553 |             ByKeyID(ref keyid) => self.link_by_keyid(keyid),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/fs.rs:553:13
    |
553 |             ByKeyID(ref keyid) => self.link_by_keyid(keyid),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
553 -             ByKeyID(ref keyid) => self.link_by_keyid(keyid),
553 +             ByKeyID(keyid) => self.link_by_keyid(keyid),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/fs.rs:554:21
    |
554 |             ByEmail(ref email) => self.link_by_email(email),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/fs.rs:554:13
    |
554 |             ByEmail(ref email) => self.link_by_email(email),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
554 -             ByEmail(ref email) => self.link_by_email(email),
554 +             ByEmail(email) => self.link_by_email(email),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/sqlite.rs:278:27
    |
278 |             ByFingerprint(ref fp) => query_simple(
    |                           ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/sqlite.rs:278:13
    |
278 |             ByFingerprint(ref fp) => query_simple(
    |             ^^^^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
278 -             ByFingerprint(ref fp) => query_simple(
278 +             ByFingerprint(fp) => query_simple(
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/sqlite.rs:283:21
    |
283 |             ByKeyID(ref keyid) => query_simple(
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/sqlite.rs:283:13
    |
283 |             ByKeyID(ref keyid) => query_simple(
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
283 -             ByKeyID(ref keyid) => query_simple(
283 +             ByKeyID(keyid) => query_simple(
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/sqlite.rs:288:21
    |
288 |             ByEmail(ref email) => query_simple(
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/sqlite.rs:288:13
    |
288 |             ByEmail(ref email) => query_simple(
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
288 -             ByEmail(ref email) => query_simple(
288 +             ByEmail(email) => query_simple(
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/lib.rs:194:27
    |
194 |             ByFingerprint(ref fp) => self.by_fpr(fp),
    |                           ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/lib.rs:194:13
    |
194 |             ByFingerprint(ref fp) => self.by_fpr(fp),
    |             ^^^^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
194 -             ByFingerprint(ref fp) => self.by_fpr(fp),
194 +             ByFingerprint(fp) => self.by_fpr(fp),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/lib.rs:195:21
    |
195 |             ByKeyID(ref keyid) => self.by_kid(keyid),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/lib.rs:195:13
    |
195 |             ByKeyID(ref keyid) => self.by_kid(keyid),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
195 -             ByKeyID(ref keyid) => self.by_kid(keyid),
195 +             ByKeyID(keyid) => self.by_kid(keyid),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/lib.rs:196:21
    |
196 |             ByEmail(ref email) => self.by_email(email),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/lib.rs:196:13
    |
196 |             ByEmail(ref email) => self.by_email(email),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
196 -             ByEmail(ref email) => self.by_email(email),
196 +             ByEmail(email) => self.by_email(email),
    |

Changes:
- Remove unnecessary "ref" binding modifiers in match statements as in
  later Rust editions so called match ergonomics modified binding
  behavior and "ref" when matching reference is not needed any more.
2025-04-27 02:04:47 +02:00
Zeke Fast
e90a2e2888 Fix broken import of HagridConfig after switch to 2024 edition in "hagridctl" crate.
Changes:
- Imports crate::HagridConfig instead of HagridConfig.
2025-04-27 02:04:47 +02:00
Zeke Fast
ee82a078ea Fix broken imports of Result after switch to 2024 edition in "database" crate.
As in 2015 edition we don't need to specify precise import path imports
like "use Result;" led to usage of imported in "lib.rs" anyhow::Result.
After switch to 2024 edition Rust requires to specify path precisely
(actually it starts require that in 2018 I guess).
So, after the switch compiler confuses "use Result;" with import of "use
core::result::Result;" and starts to require additional generic.
Simply pointing to "crate::Result" fixes the problem and points back to
anyhow::Result imported in lib.rs file.

The following compile errors were fixed:

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:54:34
    |
54  |     fn try_from(uid: &UserID) -> Result<Self> {
    |                                  ^^^^^^ ---- supplied 1 generic argument
    |                                  |
    |                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
54  |     fn try_from(uid: &UserID) -> Result<Self, E> {
    |                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sync.rs:17:44
    |
17  |     pub fn lock(path: impl AsRef<Path>) -> Result<Self> {
    |                                            ^^^^^^ ---- supplied 1 generic argument
    |                                            |
    |                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
17  |     pub fn lock(path: impl AsRef<Path>) -> Result<Self, E> {
    |                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:59:59
    |
59  |     pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self> {
    |                                                           ^^^^^^ ---- supplied 1 generic argument
    |                                                           |
    |                                                           expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
59  |     pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self, E> {
    |                                                                      +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:72:10
    |
72  |     ) -> Result<Self> {
    |          ^^^^^^ ---- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
72  |     ) -> Result<Self, E> {
    |                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:81:10
    |
81  |     ) -> Result<Self> {
    |          ^^^^^^ ---- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
81  |     ) -> Result<Self, E> {
    |                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:281:10
    |
281 |     ) -> Result<()> {
    |          ^^^^^^ -- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
281 |     ) -> Result<(), E> {
    |                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:350:67
    |
350 |     fn link_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                   ^^^^^^ -- supplied 1 generic argument
    |                                                                   |
    |                                                                   expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
350 |     fn link_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                            +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:362:67
    |
362 |     fn link_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                   ^^^^^^ -- supplied 1 generic argument
    |                                                                   |
    |                                                                   expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
362 |     fn link_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                            +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:374:69
    |
374 |     fn unlink_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                     ^^^^^^ -- supplied 1 generic argument
    |                                                                     |
    |                                                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
374 |     fn unlink_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                              +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:386:69
    |
386 |     fn unlink_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                     ^^^^^^ -- supplied 1 generic argument
    |                                                                     |
    |                                                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
386 |     fn unlink_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                              +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:402:24
    |
402 |     fn commit(self) -> Result<()> {
    |                        ^^^^^^ -- supplied 1 generic argument
    |                        |
    |                        expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
402 |     fn commit(self) -> Result<(), E> {
    |                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:406:48
    |
406 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert> {
    |                                                ^^^^^^ -------------- supplied 1 generic argument
    |                                                |
    |                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
406 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert, E> {
    |                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:415:76
    |
415 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                                            |
    |                                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
415 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:425:81
    |
425 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                                 |
    |                                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
425 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:439:10
    |
439 |     ) -> Result<()> {
    |          ^^^^^^ -- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
439 |     ) -> Result<(), E> {
    |                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:454:73
    |
454 |     fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()> {
    |                                                                         ^^^^^^ -- supplied 1 generic argument
    |                                                                         |
    |                                                                         expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
454 |     fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<(), E> {
    |                                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:467:63
    |
467 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                               ^^^^^^ -- supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
467 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:478:65
    |
478 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                 |
    |                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
478 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:484:74
    |
484 |     fn link_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                          ^^^^^^ -- supplied 1 generic argument
    |                                                                          |
    |                                                                          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
484 |     fn link_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:501:76
    |
501 |     fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                                            |
    |                                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
501 |     fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:528:33
    |
528 |     fn transaction(&'a self) -> Result<FilesystemTransaction<'a>> {
    |                                 ^^^^^^ ------------------------- supplied 1 generic argument
    |                                 |
    |                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
528 |     fn transaction(&'a self) -> Result<FilesystemTransaction<'a>, E> {
    |                                                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:536:78
    |
536 |     fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()> {
    |                                                                              ^^^^^^ -- supplied 1 generic argument
    |                                                                              |
    |                                                                              expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
536 |     fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<(), E> {
    |                                                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:604:37
    |
604 |     fn get_last_log_entry(&self) -> Result<Fingerprint> {
    |                                     ^^^^^^ ----------- supplied 1 generic argument
    |                                     |
    |                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
604 |     fn get_last_log_entry(&self) -> Result<Fingerprint, E> {
    |                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:624:10
    |
624 |     ) -> Result<Option<Fingerprint>> {
    |          ^^^^^^ ------------------- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
624 |     ) -> Result<Option<Fingerprint>, E> {
    |                                    +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:659:36
    |
659 |     fn check_consistency(&self) -> Result<()> {
    |                                    ^^^^^^ -- supplied 1 generic argument
    |                                    |
    |                                    expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
659 |     fn check_consistency(&self) -> Result<(), E> {
    |                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:32:54
    |
32  |     pub fn new_file(base_dir: impl Into<PathBuf>) -> Result<Self> {
    |                                                      ^^^^^^ ---- supplied 1 generic argument
    |                                                      |
    |                                                      expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
32  |     pub fn new_file(base_dir: impl Into<PathBuf>) -> Result<Self, E> {
    |                                                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:144:24
    |
144 |     fn commit(self) -> Result<()> {
    |                        ^^^^^^ -- supplied 1 generic argument
    |                        |
    |                        expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
144 |     fn commit(self) -> Result<(), E> {
    |                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:150:48
    |
150 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert> {
    |                                                ^^^^^^ -------------- supplied 1 generic argument
    |                                                |
    |                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
150 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert, E> {
    |                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:154:76
    |
154 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                                            |
    |                                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
154 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:171:81
    |
171 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                                 |
    |                                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
171 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:188:10
    |
188 |     ) -> Result<()> {
    |          ^^^^^^ -- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
188 |     ) -> Result<(), E> {
    |                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:200:75
    |
200 |     fn write_to_quarantine(&self, _fpr: &Fingerprint, _content: &[u8]) -> Result<()> {
    |                                                                           ^^^^^^ -- supplied 1 generic argument
    |                                                                           |
    |                                                                           expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
200 |     fn write_to_quarantine(&self, _fpr: &Fingerprint, _content: &[u8]) -> Result<(), E> {
    |                                                                                    +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:204:63
    |
204 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                               ^^^^^^ -- supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
204 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:221:65
    |
221 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                 |
    |                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
221 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:231:78
    |
231 |     fn link_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                              ^^^^^^ -- supplied 1 generic argument
    |                                                                              |
    |                                                                              expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
231 |     fn link_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:252:80
    |
252 |     fn unlink_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                                ^^^^^^ -- supplied 1 generic argument
    |                                                                                |
    |                                                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
252 |     fn unlink_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:264:33
    |
264 |     fn transaction(&'a self) -> Result<Self::Transaction> {
    |                                 ^^^^^^ ----------------- supplied 1 generic argument
    |                                 |
    |                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
264 |     fn transaction(&'a self) -> Result<Self::Transaction, E> {
    |                                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:268:80
    |
268 |     fn write_log_append(&self, _filename: &str, _fpr_primary: &Fingerprint) -> Result<()> {
    |                                                                                ^^^^^^ -- supplied 1 generic argument
    |                                                                                |
    |                                                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
268 |     fn write_log_append(&self, _filename: &str, _fpr_primary: &Fingerprint) -> Result<(), E> {
    |                                                                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:402:10
    |
402 |     ) -> Result<Option<Fingerprint>> {
    |          ^^^^^^ ------------------- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
402 |     ) -> Result<Option<Fingerprint>, E> {
    |                                    +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:411:36
    |
411 |     fn check_consistency(&self) -> Result<()> {
    |                                    ^^^^^^ -- supplied 1 generic argument
    |                                    |
    |                                    expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
411 |     fn check_consistency(&self) -> Result<(), E> {
    |                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:476:37
    |
476 |     fn get_last_log_entry(&self) -> Result<Fingerprint> {
    |                                     ^^^^^^ ----------- supplied 1 generic argument
    |                                     |
    |                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
476 |     fn get_last_log_entry(&self) -> Result<Fingerprint, E> {
    |                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/stateful_tokens.rs:14:50
    |
14  |     pub fn new(token_dir: impl Into<PathBuf>) -> Result<Self> {
    |                                                  ^^^^^^ ---- supplied 1 generic argument
    |                                                  |
    |                                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
14  |     pub fn new(token_dir: impl Into<PathBuf>) -> Result<Self, E> {
    |                                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:97:29
    |
97  |     fn from_str(s: &str) -> Result<Email> {
    |                             ^^^^^^ ----- supplied 1 generic argument
    |                             |
    |                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
97  |     fn from_str(s: &str) -> Result<Email, E> {
    |                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:124:55
    |
124 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
    |                                                       ^^^^^^ ---- supplied 1 generic argument
    |                                                       |
    |                                                       expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
124 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self, E> {
    |                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:163:29
    |
163 |     fn from_str(s: &str) -> Result<Fingerprint> {
    |                             ^^^^^^ ----------- supplied 1 generic argument
    |                             |
    |                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
163 |     fn from_str(s: &str) -> Result<Fingerprint, E> {
    |                                               +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:196:55
    |
196 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
    |                                                       ^^^^^^ ---- supplied 1 generic argument
    |                                                       |
    |                                                       expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
196 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self, E> {
    |                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:232:29
    |
232 |     fn from_str(s: &str) -> Result<KeyID> {
    |                             ^^^^^^ ----- supplied 1 generic argument
    |                             |
    |                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
232 |     fn from_str(s: &str) -> Result<KeyID, E> {
    |                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:52:34
    |
52  | fn ensure_parent(path: &Path) -> Result<&Path> {
    |                                  ^^^^^^ ----- supplied 1 generic argument
    |                                  |
    |                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
52  | fn ensure_parent(path: &Path) -> Result<&Path, E> {
    |                                              +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:319:60
    |
319 | fn symlink(symlink_content: &Path, symlink_name: &Path) -> Result<()> {
    |                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                            |
    |                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
319 | fn symlink(symlink_content: &Path, symlink_name: &Path) -> Result<(), E> {
    |                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:334:63
    |
334 | fn symlink_unlink_with_check(link: &Path, expected: &Path) -> Result<()> {
    |                                                               ^^^^^^ -- supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
334 | fn symlink_unlink_with_check(link: &Path, expected: &Path) -> Result<(), E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:268:48
    |
268 |     fn open_logfile(&self, file_name: &str) -> Result<File> {
    |                                                ^^^^^^ ---- supplied 1 generic argument
    |                                                |
    |                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
268 |     fn open_logfile(&self, file_name: &str) -> Result<File, E> {
    |                                                           +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:280:55
    |
280 |         check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<()>,
    |                                                       ^^^^^^ -- supplied 1 generic argument
    |                                                       |
    |                                                       expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
280 |         check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<(), E>,
    |                                                                +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:65:56
    |
65  |     fn build_pool(manager: SqliteConnectionManager) -> Result<r2d2::Pool<SqliteConnectionManager>> {
    |                                                        ^^^^^^ ----------------------------------- supplied 1 generic argument
    |                                                        |
    |                                                        expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
65  |     fn build_pool(manager: SqliteConnectionManager) -> Result<r2d2::Pool<SqliteConnectionManager>, E> {
    |                                                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:69:77
    |
69  |     fn new_internal(base_dir: PathBuf, manager: SqliteConnectionManager) -> Result<Self> {
    |                                                                             ^^^^^^ ---- supplied 1 generic argument
    |                                                                             |
    |                                                                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
69  |     fn new_internal(base_dir: PathBuf, manager: SqliteConnectionManager) -> Result<Self, E> {
    |                                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:117:61
    |
117 |     fn start(pool: &r2d2::Pool<SqliteConnectionManager>) -> Result<Self> {
    |                                                             ^^^^^^ ---- supplied 1 generic argument
    |                                                             |
    |                                                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
117 |     fn start(pool: &r2d2::Pool<SqliteConnectionManager>) -> Result<Self, E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/stateful_tokens.rs:24:66
    |
24  |     pub fn new_token(&self, token_type: &str, payload: &[u8]) -> Result<String> {
    |                                                                  ^^^^^^ ------ supplied 1 generic argument
    |                                                                  |
    |                                                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
24  |     pub fn new_token(&self, token_type: &str, payload: &[u8]) -> Result<String, E> {
    |                                                                               +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/stateful_tokens.rs:41:63
    |
41  |     pub fn pop_token(&self, token_type: &str, token: &str) -> Result<String> {
    |                                                               ^^^^^^ ------ supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
41  |     pub fn pop_token(&self, token_type: &str, token: &str) -> Result<String, E> {
    |                                                                            +++

Changes:
- Fix broken imports of Result in "database" crate by importing
  crate::Result instead of just Result.
2025-04-27 02:04:47 +02:00
Zeke Fast
50f80ebade Fix imports after migration to 2024 edition.
Differences in Rust edition lead to compilation errors like the ones
below:

error[E0432]: unresolved import `sync`
  --> database/src/fs.rs:15:5
   |
15 | use sync::FlockMutexGuard;
   |     ^^^^ help: a similar path exists: `crate::sync`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved import `types`
  --> database/src/fs.rs:16:5
   |
16 | use types::{Email, Fingerprint, KeyID};
   |     ^^^^^ help: a similar path exists: `crate::types`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved imports `Database`, `Query`
  --> database/src/fs.rs:18:6
   |
18 | use {Database, Query};
   |      ^^^^^^^^  ^^^^^ no external crate `Query`
   |      |
   |      no external crate `Database`
   |
   = help: consider importing this trait instead:
           crate::Database
   = help: consider importing this enum instead:
           crate::Query

error[E0432]: unresolved import `wkd`
  --> database/src/fs.rs:20:5
   |
20 | use wkd;
   |     ^^^ no external crate `wkd`
   |
help: consider importing this module instead
   |
20 | use crate::wkd;
   |     +++++++

error[E0432]: unresolved import `openpgp_utils`
  --> database/src/fs.rs:25:5
   |
25 | use openpgp_utils::POLICY;
   |     ^^^^^^^^^^^^^ help: a similar path exists: `crate::openpgp_utils`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved import `types`
  --> database/src/sqlite.rs:11:5
   |
11 | use types::{Email, Fingerprint, KeyID};
   |     ^^^^^ help: a similar path exists: `crate::types`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved imports `Database`, `Query`
  --> database/src/sqlite.rs:13:6
   |
13 | use {Database, Query};
   |      ^^^^^^^^  ^^^^^ no external crate `Query`
   |      |
   |      no external crate `Database`
   |
   = help: consider importing this trait instead:
           crate::Database
   = help: consider importing this enum instead:
           crate::Query

error[E0432]: unresolved import `Email`
 --> database/src/openpgp_utils.rs:9:5
  |
9 | use Email;
  |     ^^^^^ no external crate `Email`
  |
help: consider importing this struct through its public re-export instead
  |
9 | use crate::Email;
  |     +++++++

Changes:
- Prefix imports with "crate::" as compiler suggest to fix the errors.
2025-04-27 02:04:47 +02:00
Zeke Fast
709e358800 Change edition in Cargo.toml files to 2024.
Changes:
- Change edition in the following Cargo.toml files to 2024:
  - Cargo.toml change edition: 2018 -> 2024
  - Explicitly set 2024 (i.e. default 2015 -> 2024) edition
    in the following files:
    - database/Cargo.toml
    - hagridctl/Cargo.toml
    - tester/Cargo.toml
    NOTE: setting explicitly edition also clean up WARNINGS like ones bellow:

    warning: .../hagrid/database/Cargo.toml: no edition set: defaulting to the 2015 edition while the latest is 2024
    warning: .../hagrid/tester/Cargo.toml: no edition set: defaulting to the 2015 edition while the latest is 2024
    warning: .../hagrid/hagridctl/Cargo.toml: no edition set: defaulting to the 2015 edition while the latest is 2024
2025-04-27 02:04:46 +02:00
Zeke Fast
da6267887e Upgrade used Rust version in rust-toolchain.toml file: 1.82 -> 1.86. 2025-04-27 02:04:46 +02:00
Zeke Fast
31f4ff704f Migrate to newer toolchain configuration file: rust-toolchain -> rust-toolchain.toml.
Documentation: https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file

Changes:
- Rename toolchain file: rust-toolchain -> rust-toolchain.toml.
- Format rust-toolchain.toml in TOML as required.
2025-04-27 02:04:46 +02:00
Vincent Breitmoser
871cae1e24 version 2.0.1 2025-03-25 09:23:57 +01:00
Vincent Breitmoser
f024b0bffe nix+web: pass in commit hash when building from flake 2025-03-25 09:22:50 +01:00
Vincent Breitmoser
a5294b07cb nginx: simplify routes for hagrid v2 2025-03-24 22:49:11 +01:00
Vincent Breitmoser
c6aa0b3fdb docker: remove
I no longer use this, so it won't be maintained
2025-03-24 22:48:39 +01:00
Vincent Breitmoser
3bceb608e8 fix wkd domain checker 2025-03-15 13:59:50 +01:00
Vincent Breitmoser
f6b1f3cc73 wkd: update a bit, and add to flake 2025-02-28 22:52:26 +01:00
Vincent Breitmoser
3293dd8f78 version 2.0 2025-02-28 22:07:16 +01:00
Vincent Breitmoser
c7a032eb69 nix: add nix flake 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
475bcbffb8 nginx: route all requests via hagrid 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
dafed3d492 db: don't use sq's export logic for our certs 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
a504b0ea12 hagridctl: update for sqlite 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
df6bfb2d84 db: improve typings for sqlite 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
b5b5879474 db: add DatabaseTransaction abstraction 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
5778aaed84 db: work on sqlite, make tests pass 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
7beb5209af db: add sqlite query tracing during tests 2025-02-28 22:05:32 +01:00
puzzlewolf
4787816581 db: start work on rusqlite 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
359475f89f docker: add sqlite dep 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
253d672d47 db: abstract over log path interface 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
e0aeef7ddc mail: support sending via local smtp server 2025-02-28 21:53:25 +01:00
Vincent Breitmoser
44db398a1c cargo: downgrade sequoia-openpgp to 1.17.0 (for now)
Starting with 1.18.0, the retain_userids method starts working
differently, returning an empty cert if no signed user ids or direct key
signature is left. Since we need this, we'll stay on 1.17.0 for now.
2024-11-17 19:08:23 +01:00
Vincent Breitmoser
8ea89d3e0e hagrid: fix tokens test 2024-11-17 14:15:34 +01:00
Vincent Breitmoser
0d25da7138 cargo: cargo fmt --all 2024-11-17 14:03:12 +01:00
Vincent Breitmoser
e0f8352ac6 docker: update docker-build for new rust-toolchain 2024-11-17 13:49:44 +01:00
Vincent Breitmoser
dca8afa1e6 cargo: cargo update 2024-11-17 13:47:22 +01:00
Vincent Breitmoser
ea44f52a16 rust-toolchain: update to 1.82.0 2024-11-17 13:46:25 +01:00
Vincent Breitmoser
b4d92f0ec1 use rust-crypto instead of ring for sealed state
Newer versions of ring are very obscure, and I couldn't figure out how
to use its interface in a reasonable time. I used the rust-crypto
methods instead where things were straightforward.
2024-11-17 13:46:24 +01:00
Vincent Breitmoser
26ef2f6e1c db: fix tests 2024-03-24 23:50:56 +01:00
Vincent Breitmoser
cfd9fd8eb3 tester: add gen-reqs command 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
13ddd4ff3a tester: add tester workspace, adding tools for testing 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
a9440c6d0a hagridctl: add dump command to dump entire database 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
fe2337507a hagridctl: import public keys publishing emails 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
36dff563fc docker: use bullseye base image 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
da5648488b ci: actually use correct dep package name 2024-01-27 10:24:26 +01:00
Vincent Breitmoser
7f304929ea ci: update gitlab for openssl dep 2024-01-27 10:22:47 +01:00
Vincent Breitmoser
7c7b15e37c version 1.3.0 2024-01-26 15:35:37 +01:00
Vincent Breitmoser
dfafe5cdb7 cargo: use openssl crypo backend 2024-01-26 15:35:37 +01:00
Vincent Breitmoser
45c6fcf216 cargo: simplify versions of hagridctl as well 2024-01-26 15:33:41 +01:00
Vincent Breitmoser
e85d3eab40 nix: update for 23.11 2024-01-12 12:15:11 +01:00
Vincent Breitmoser
defd2314be docker: add instructions to build for a Debian environment via docker 2023-12-28 13:52:49 +01:00
Vincent Breitmoser
da4665306e cargo: cargo update
This update requires a forked version of rocket_i18n to accommodate for
a trivial renaming in rocket v0.5.0. Can be changed back to upstream if
https://github.com/Plume-org/rocket_i18n/pull/24 is merged.
2023-12-28 13:37:45 +01:00
Vincent Breitmoser
d11de8a354 hagrid: don't panic on short token size 2023-12-28 12:54:04 +01:00
Vincent Breitmoser
1d1eedc319 about: update privacy policy
Also clear translated files for this particular template.
2023-06-30 13:30:19 +02:00
Vincent Breitmoser
e7ec0edf1e db: check that user ids contain a valid self-signature for publication 2023-06-10 14:36:04 +00:00
Vincent Breitmoser
ed624da4a4 cargo: cargo update 2023-06-10 16:16:04 +02:00
Vincent Breitmoser
831331fd2d about: add news entry about k.o.o governance 2023-06-10 16:04:06 +02:00
Vic Demuzere
7f92f1813b Fix links to renamed Gitlab project
Most of them redirected to the new location but the commit link at the
bottom of every page resulted in a 404.
2023-06-05 11:37:36 +02:00
Vincent Breitmoser
37d42e96d7 i18n: fix some issues with position arguments 2023-02-11 17:57:54 +01:00
Vincent Breitmoser
e96594ab26 remove some stray println statements 2023-02-11 17:39:37 +01:00
Vincent Breitmoser
55e54c8bab readme: add notice about maintenance 2023-02-11 17:39:37 +01:00
Vincent Breitmoser
04b4bc817a i18n: update some fuzzily misdetected strings 2022-12-10 17:42:56 +01:00
Vincent Breitmoser
5e08a7086e cargo: cargo update and fix for some deps 2022-12-10 17:42:50 +01:00
Vincent Breitmoser
875ab41c7d cargo: apply cargo fmt --all 2022-12-10 15:29:58 +01:00
Justus Winter
dc2d67d9eb Bump sequoia-openpgp to 1.11 and synchronize src/dump.rs. 2022-12-09 12:59:42 +01:00
Vincent Breitmoser
0a28c04f86 contrib: remove systemd service file 2022-06-29 10:57:13 +02:00
Vincent Breitmoser
aca0cdcdff version 1.2.1 2022-04-09 13:37:51 +02:00
Vincent Breitmoser
aa6474fd29 cargo: cargo update 2022-04-09 13:36:33 +02:00
Vincent Breitmoser
55ec155b30 mail: update to lettre-0.10.0-rc.5 and adapt 2022-04-09 13:36:33 +02:00
Vincent Breitmoser
39fae28f53 version 1.2.0 2022-03-12 15:30:51 +01:00
Vincent Breitmoser
427fe351d8 i18n: tx pull 2022-03-12 15:20:12 +01:00
Vincent Breitmoser
7e00c56a24 dist: small css fixes 2022-03-10 18:09:10 +01:00
Nora Widdecke
07bab9d11b hagridtcl: Allow clippy::needless_collect 2022-03-06 23:30:12 +00:00
Nora Widdecke
e00cae5a4e db,hagridctl: Autofix clippy issues 2022-03-06 23:30:12 +00:00
Nora Widdecke
1802cc6811 Run clippy on the whole workspace 2022-03-06 23:30:12 +00:00
Nora Widdecke
59c42c033d Apply clippy to the tests, too 2022-03-06 23:30:12 +00:00
Nora Widdecke
ed924f439b hkp: Cleanup pks/lookup route.
- Replace custom FromRequest implementation and query parsing with
    rocket builtin.

  - Remove Hkp::Invalid variant, replaced with Result
2022-02-28 17:47:41 +01:00
Nora Widdecke
3b2810dcf7 hkp: Fix pks/lookup with urlencoded parameter.
And test the pks/lookup route with a urlencoded parameter.

  Fixes #168.
2022-02-28 17:42:22 +01:00
Vincent Breitmoser
7b8f5e1462 i18n: commit translation files for rustfmt determined compilation order 2022-02-26 18:47:32 +01:00
Vincent Breitmoser
a9c4786d14 i18n: extract include_i18n macro use into method for consistent compilation order 2022-02-26 18:23:22 +01:00
Vincent Breitmoser
e4718d7598 ci: simplify ci pipeline, check formatting 2022-02-26 18:20:55 +01:00
Vincent Breitmoser
b29845b893 cargo: apply cargo fmt --all 2022-02-26 17:01:14 +01:00
Vincent Breitmoser
961559e154 cargo: remove rustfmt config 2022-02-26 17:01:14 +01:00
Percy
4d8bb36824 fix: the text in the search button is not displayed in the small screen 2022-02-26 15:44:33 +00:00
Percy
2ec10fc40a fix: link and image overflow 2022-02-26 15:44:33 +00:00
Nora Widdecke
c1a88f8840 web: handle wkd requests 2022-02-26 16:40:54 +01:00
Vincent Breitmoser
329a9c09b0 update gettext-macros to 0.6.1 2022-02-26 16:34:57 +01:00
Nora Widdecke
36b03ea608 ci: Run clippy 2022-02-25 10:25:26 +01:00
Nora Widdecke
b06c2c96bd lint: allow clippy::nonminimal_bool 2022-02-25 10:25:26 +01:00
Nora Widdecke
46646f1965 lint: raise too-many-arguments-threshold 2022-02-25 10:25:26 +01:00
Nora Widdecke
a329a1a89a lint: fix clippy::single_component_path_imports 2022-02-25 10:25:26 +01:00
Nora Widdecke
cd020cae40 lint: fix clippy::collapsible_if 2022-02-25 10:25:26 +01:00
Nora Widdecke
b2a7ca29b7 lint: fix clippy::clone_on_copy 2022-02-25 10:25:26 +01:00
Nora Widdecke
5a6d1a97fd lint: fix clippy::bind_instead_of_map 2022-02-25 10:25:26 +01:00
Nora Widdecke
d2f6d682ac lint: fix clippy::needless_lifetimes 2022-02-25 10:25:26 +01:00
Nora Widdecke
4d27f3f5b9 lint: fix clippy::single_match 2022-02-25 10:25:26 +01:00
Nora Widdecke
a46bd4ebee lint: fix clippy::redundant_closure 2022-02-25 10:25:26 +01:00
Nora Widdecke
3253f50127 lint: fix clippy::match_like_matches_macro 2022-02-25 10:25:26 +01:00
Nora Widdecke
a2ace61e71 lint: fix clippy::or_fun_call 2022-02-25 10:25:26 +01:00
Nora Widdecke
421f8a0908 lint: Apply clippy autofixes
clippy::needless_borrow

  clippy::single_char_pattern

  clippy::redundant_clone

  clippy::needless_return

  clippy::needless_question_mark

  clippy::useless_conversion

  clippy::to_string_in_format_args

  clippy::to_string_in_format_args

  clippy::useless_format

  clippy::useless_vec

  clippy::toplevel_ref_arg

  clippy::redundant_static_lifetimes

  clippy::try_err
2022-02-25 10:23:34 +01:00
Nora Widdecke
8eb3984560 cargo: use rocket_prometheus 0.10.0-rc.1
Fixes #167
2022-02-15 11:35:13 +01:00
Vincent Breitmoser
546e3b9452 readme: update for stable rust 2022-02-06 23:33:33 +01:00
Justus Winter
57efbe2937 hagridctl: drop backtrace from error messages 2022-02-06 22:58:29 +01:00
Justus Winter
81b5426544 hagridctl: drop feature and recursion_limit attribute 2022-02-06 22:58:29 +01:00
Justus Winter
f1078b3ccc hagridctl: adapt to rockets new profile names 2022-02-06 22:58:29 +01:00
Justus Winter
42260ff2e1 web: change profile names to match rocket's new convention
see https://rocket.rs/v0.5-rc/guide/configuration/#default-provider
2022-02-06 22:58:29 +01:00
Justus Winter
7b413150ca web: start from rocket's default config for the tests 2022-02-06 22:58:29 +01:00
Justus Winter
11f93c3249 web: fix extracting configuration values 2022-02-06 22:58:29 +01:00
Justus Winter
fec0cc4852 web: port to handlebars 3, the version used in rocket's dyn templates 2022-02-06 22:58:29 +01:00
Justus Winter
b97a06f51c web: rename request_origin -> origin, it is a mouthful already 2022-02-06 22:58:29 +01:00
Justus Winter
cf0abbe047 web: immutable responses 2022-02-06 22:58:29 +01:00
Justus Winter
2bf703a796 web: don't use consumed response 2022-02-06 22:58:29 +01:00
Justus Winter
a31d69d111 web: enable prometheus again 2022-02-06 22:58:29 +01:00
Justus Winter
b428116189 web: i18n and request origin everywhere 2022-02-06 22:58:29 +01:00
Justus Winter
24eb0b0d1b web: anyhow::Error doesn't implement Responder 2022-02-06 22:58:29 +01:00
Justus Winter
23fb3f9fb2 web: fix header composition 2022-02-06 22:58:29 +01:00
Justus Winter
f50ce6912a cargo: use published rocket_i18n, hyperx 2022-02-06 22:58:29 +01:00
Vincent Breitmoser
a2bc5f014c web: first iteration, update to rocket v0.5-rc1 2022-02-06 22:58:29 +01:00
Vincent Breitmoser
3f156ec8c2 cargo: update, and use rocket v0.5-rc1 2022-02-06 22:58:22 +01:00
Vincent Breitmoser
33224d1855 i18n: fix tests and strings 2022-01-04 15:55:01 +01:00
Vincent Breitmoser
23880d1386 db+web: remove x-accel optimization
This removes a shortcut to serve certificates from nginx by including an
X-Accel-Redirect header in the response.
2022-01-04 13:52:58 +01:00
Vincent Breitmoser
77407e03cc db: correctly abstract NamedTempFile as type trait 2022-01-04 13:28:26 +01:00
Vincent Breitmoser
6782c57520 fs: group abstract methods on top of Database trait 2022-01-04 13:23:37 +01:00
Vincent Breitmoser
0e0b5c160a cargo: cargo update, and use recent nightly 2022-01-04 12:24:44 +01:00
Vincent Breitmoser
3aa26c10f3 nix: update shell.nix 2022-01-04 12:17:18 +01:00
Vincent Breitmoser
9d5ec287a9 cargo: update gettext-macros to patched 0.6
This should work with rust stable, once we update rocket.

Uses a patched version of gettext-macros with a bugfix for
https://github.com/Plume-org/gettext-macros/issues/16
2022-01-04 12:06:16 +01:00
Vincent Breitmoser
43cdb28b97 i18n: tx pull 2021-11-28 14:32:14 +01:00
Justus Winter
b8ddf58977 update sequoia-openpgp to 1.5 2021-10-25 16:04:12 +02:00
Justus Winter
6db41b87f2 update dump.rs from sq, revert to its canonical form
Previously, the code was taken with the modifications from
dump.sequoia-pgp.org.  However, the canonical form is the one from the
sq tool.
2021-10-11 11:06:13 +02:00
Justus Winter
8dabd2c37a update sequoia-openpgp to 1.4 2021-10-11 10:23:45 +02:00
Vincent Breitmoser
90356ddb28 update changed files from rebuild 2021-07-21 09:44:43 +02:00
Justus Winter
bb9a3d8324 Strip non-exportable signatures and cert components.
If non-exportable signatures are uploaded to Hagrid, this is most
certainly an accident.  Handle this gracefully by stripping these
signatures (and certificate components that are only bound by
non-exportable signatures) when writing them to the database.

Fixes #155.
2021-07-15 19:21:44 +00:00
Vincent Breitmoser
0543e13b14 nginx: re-add missing proxy cache path directive 2021-07-14 12:32:14 +02:00
Vincent Breitmoser
3432fbe584 readme: small update
Mention use of ngx_http_lua_module for nginx config, and document move
of the IRC channel to OFTC.
2021-07-13 11:09:00 +02:00
Vincent Breitmoser
569a9df5a0 nginx: update nginx.conf, ditch nginx-site.conf 2021-07-13 11:05:44 +02:00
238 changed files with 14725 additions and 14824 deletions

1
.envrc
View File

@@ -1 +1,2 @@
watch_file rust-toolchain
use nix

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@
target
*.po~
/dist/templates/localized
result

View File

@@ -1,14 +1,12 @@
stages:
- build
build:binary:
stage: build
tags:
- docker
image: "rustlang/rust:nightly"
build, test and lint:
image: "rust:1.86-bookworm"
interruptible: true
script:
- apt update -qy
- apt install -qy libclang-dev build-essential pkg-config clang nettle-dev gettext zsh
- ./make-translated-templates
- RUST_BACKTRACE=full cargo build
- RUST_BACKTRACE=full cargo test --all
- apt install -qy build-essential pkg-config clang libclang-dev libssl-dev gettext zsh
- rustup component add clippy
- rustup component add rustfmt
- cargo build
- cargo clippy --tests --no-deps --workspace
- cargo fmt --all -- --check
- cargo test --workspace

View File

@@ -8,52 +8,3 @@ source_lang = en
file_filter = po/hagrid/<lang>.po
trans.zh-Hans = po/hagrid/zh_Hans.po
type = PO
[hagrid.about-about]
minimum_perc = 100
source_file = templates-untranslated/about/about.html.hbs
file_filter = templates-translated/<lang>/about/about.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/about.html.hbs
source_lang = en
type = HTML
[hagrid.about-faq]
minimum_perc = 100
source_file = templates-untranslated/about/faq.html.hbs
file_filter = templates-translated/<lang>/about/faq.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/faq.html.hbs
source_lang = en
type = HTML
[hagrid.about-news]
minimum_perc = 100
source_file = templates-untranslated/about/news.html.hbs
file_filter = templates-translated/<lang>/about/news.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/news.html.hbs
source_lang = en
type = HTML
[hagrid.about-privacy]
minimum_perc = 100
source_file = templates-untranslated/about/privacy.html.hbs
file_filter = templates-translated/<lang>/about/privacy.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/privacy.html.hbs
source_lang = en
type = HTML
[hagrid.about-stats]
minimum_perc = 100
source_file = templates-untranslated/about/stats.html.hbs
file_filter = templates-translated/<lang>/about/stats.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/stats.html.hbs
source_lang = en
type = HTML
[hagrid.about-usage]
minimum_perc = 100
source_file = templates-untranslated/about/usage.html.hbs
file_filter = templates-translated/<lang>/about/usage.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/usage.html.hbs
source_lang = en
type = HTML

4211
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,104 @@
[package]
name = "hagrid"
version = "1.1.0"
version = "2.1.0"
authors = ["Vincent Breitmoser <look@my.amazin.horse>", "Kai Michaelis <kai@sequoia-pgp.org>", "Justus Winter <justus@sequoia-pgp.org>"]
build = "build.rs"
default-run = "hagrid"
edition = "2018"
edition = "2024"
rust-version = "1.86"
resolver = "3"
[workspace]
members = [
"database",
"hagridctl",
"tester",
]
[dependencies]
hagrid-database = { path = "database" }
chrono = "0.4.10"
[workspace.dependencies]
anyhow = "1"
rocket = "0"
rocket_codegen = "0"
sequoia-openpgp = { version = "1.3", default-features = false, features = ["crypto-nettle"] }
multipart = "0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
time = "0.1"
tempfile = "3.0"
structopt = "0.2"
url = "1.6"
handlebars = "1.1.0"
num_cpus = "1.0"
ring = "0.13"
hagrid-database = { path = "database" }
aes-gcm = "0.10"
base64 = "0.10"
uuid = { version = "0.7", features = [ "v4" ] }
rocket_prometheus = "0.2"
lazy_static = "1.3.0"
rocket_i18n = "0.4"
gettext-macros = "0.5"
runtime-fmt = "0.4"
chrono = "0.4"
clap = ">= 4.5.37"
fs2 = "0.4"
gettext = "0.4"
gettext-macros = "0.6"
gettext-utils = "0.1"
glob = "0.3"
rfc2047 = "0.1"
hex = "0.3"
hyperx = "1.4"
idna = "0.1"
indicatif = "0.11"
lettre = { version = "=0.10.0-rc.5", default-features = false }
log = ">= 0.4.27"
multipart = "~0.18"
num_cpus = "1"
pathdiff = "0.1"
r2d2 = "0.8"
r2d2_sqlite = "0.24"
rand = "0.6"
regex = "1"
rocket = ">= 0.5.1"
rocket_codegen = ">= 0.5.1"
rocket_dyn_templates = ">= 0.2.0"
rocket_i18n = { git = "https://github.com/Valodim/rocket_i18n", branch = "go-async", default-features = false }
rocket_prometheus = ">= 0.10.1"
rusqlite = "0.31"
self_cell = "1"
serde = "1.0"
serde_derive = "1"
serde_json = "1"
sha2 = "0.10"
tempfile = "3"
time = "0.1"
toml = "0.5"
url = "1"
uuid = "0.7"
vergen = "3"
walkdir = "2"
zbase32 = "0.1"
sequoia-openpgp = { version = "=1.17.0", default-features = false }
rstest = ">= 0.26.1"
[patch.crates-io]
runtime-fmt = { git = "https://github.com/Valodim/runtime-fmt", rev = "44c15d832cb327ef33f95548a9a964d98c006fe4" }
[dependencies.lettre]
version = "0.10.0-pre"
default-features = false
# smtp-transport doesn't build (openssl problem)
features = ["builder", "file-transport", "sendmail-transport"]
git = "https://github.com/lettre/lettre"
rev = "245c600c82ee18b766e8729f005ff453a55dce34"
[dependencies.rocket_contrib]
version = "0"
default-features = false
features = ["handlebars_templates", "json"]
[dependencies]
hagrid-database = { workspace = true }
chrono = { workspace = true }
anyhow = { workspace = true }
rocket = { workspace = true, features = ["json"] }
rocket_dyn_templates = { workspace = true, features = ["handlebars"] }
rocket_codegen = { workspace = true }
sequoia-openpgp = { workspace = true, features = ["crypto-openssl"] }
multipart = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
time = { workspace = true }
tempfile = { workspace = true }
url = { workspace = true }
num_cpus = { workspace = true }
aes-gcm = { workspace = true }
sha2 = { workspace = true }
base64 = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
rocket_prometheus = { workspace = true }
gettext-macros = { workspace = true }
gettext-utils = { workspace = true }
gettext = { workspace = true }
glob = { workspace = true }
hyperx = { workspace = true }
# this is a slightly annoying update, so keeping this back for now
lettre = { workspace = true, features = ["builder", "file-transport", "sendmail-transport", "smtp-transport"] }
rocket_i18n= { workspace = true, features = ["rocket"] }
[build-dependencies]
vergen = "3"
vergen = { workspace = true }
[dev-dependencies]
regex = "1"
regex = { workspace = true }
rstest = { workspace = true }
[[bin]]
name = "hagrid"
path = "src/main.rs"
[[bin]]
name = "hagrid-delete"
path = "src/delete.rs"

View File

@@ -6,6 +6,15 @@ Hagrid is a verifying OpenPGP key server.
You can find general instructions and an API documentation at the running
instance at [https://keys.openpgp.org](https://keys.openpgp.org).
Please note that Hagrid is built and maintained only for the service at
keys.openpgp.org. It is not maintained or officially supported as
deployable software.
Compatibility note: Hagrid v2.0 uses an sqlite certificate store instead of the
previous file based database. This means that it also no longer supports serving
certificates directly via reverse proxy. You can us hagridctl to dump and import
an old database.
License
-------
@@ -26,22 +35,20 @@ License along with Hagrid. If not, see
Quick Start
-----------
Building Hagrid requires a working nightly Rust toolchain. The
easiest way to get the toolchain is to download [rustup](https://rustup.rs).
Building Hagrid requires a working stable Rust toolchain.
The easiest way to get the toolchain is to download [rustup](https://rustup.rs).
Additionally, install external dependencies are required. Get them (on Debian or
Ubuntu) with
Additionally, some external dependencies are required.
Get them (on Debian or Ubuntu) with
```bash
sudo apt install gnutls-bin nettle-dev gcc llvm-dev libclang-dev build-essential pkg-config gettext
sudo apt install gnutls-bin libssl-dev gcc llvm-dev libclang-dev build-essential pkg-config gettext libsqlite3-dev
```
After rustup and all other dependencies are installed, get the nightly compiler and tools, copy the
config file, and simply compile and run:
After Rust and the other dependencies are installed, copy the config file (or run `just init`), then simply compile and run:
```bash
cd hagrid
rustup override set nightly-2020-06-01
cp Rocket.toml.dist Rocket.toml
cargo run
```
@@ -53,17 +60,39 @@ will be statically built, and can be copied anywhere. You will also need to
adjust `Rocket.toml` accordingly. Hagrid uses `sendmail` for mailing, so you
also need a working local mailer setup.
Reverse Proxy
-------------
About Pages
-----------
Hagrid is designed to defer lookups to reverse proxy server like Nginx
and Apache. Lookups via `/vks/v1/by-finingerprint`, `/vks/v1/by-keyid`, and
`/vks/v1/by-email` can be handled by a robust and performant HTTP server. A
sample configuration for nginx is part of the repository (`nginx.conf`,
`hagrid-routes.conf`).
The pages under /about are built using [zola](https://getzola.org). Templates
served by hagrid assume that the built pages are available under `/about`, but
they are independent from the rest of the code.
# Development Dependencies
List of dependencies which are required or could be helpful for contribution
to the project.
| Category | Type | Name | Version | Verified Version | Notes |
|:--------------------------------:|:-----------:|:------------------------------------------:|:----------:|:----------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Shell | Obligatory | [Zsh](https://zsh.sourceforge.io/) | \>= 5.9 | 5.9 | Required for [translated templates generation](./make-translated-templates) script. |
| VCS/SCM | Obligatory | [Git](https://git-scm.com/) | \>= 2.47.3 | 2.47.3 | Obviously, if going to get this repository you'll have the `git` CLI as dependency. But it also used in `just upgrade-rust` recipe to automate Rust upgrades. |
| Shell | Preferrable | [Bash](https://www.gnu.org/software/bash/) | \>= 5.2.15 | 5.2.15 | Required for scripts embedded into [`justfile`](./justfile). If you don't want to use [`just`](https://just.systems/) you probably don't need this shell. |
| Command Runner | Preferrable | [`just`](https://just.systems/) | \>= 1.42.4 | 1.40.0 | All commands from [`justfile`](./justfile) could be run without [`just`](https://just.systems/), but it makes development more convenient. |
| SQlite Prompt | Preferrable | [`sqlite3`](https://sqlite.org/cli.html) | \>= 3.40.1 | 3.40.1 | Used by [`just db`](./justfile) recipe to open interactive prompt to SQlite database of the project. |
| Command Line HTTP client | Preferrable | [`curl`](https://curl.se/) | \>= 8.14.1 | 8.14.1 | Used by `just _rust-stable-version` recipe to determine current stable version of Rust. Indirectly, used by `just upgrade-rust` depends on `curl` through `_rust-stable-version` recipe. |
| Text stream editor | Preferrable | [`sed`](https://www.gnu.org/software/sed/) | \>= 4.9 | 4.9 | Similar to `curl`, Used by `just _rust-stable-version` recipe to determine current stable version of Rust. Indirectly, used by `just upgrade-rust` depends on `curl` through `_rust-stable-version` recipe. |
| TOML Query | Preferrable | [tomlq](https://crates.io/crates/tomlq) | \>= 0.2.2 | 0.2.2 | Similar to `curl`, Used by `just _rust-stable-version` recipe to determine current stable version of Rust. Indirectly, used by `just upgrade-rust` depends on `curl` through `_rust-stable-version` recipe. |
| Static Site Generator | Preferrable | [zola](https://www.getzola.org) | \>= 0.20.0 | 0.20.0 | Use `zola build` in aboutPages to build the about pages and serve from /about |
Community
---------
We're in `#hagrid` on Freenode IRC, also reachable via Matrix as
`#hagrid:stratum0.org`.
We're in `#hagrid` on OFTC IRC, also reachable via Matrix as `#hagrid:stratum0.org`.
# Contribution
## Housekeeping
### Rust version upgrade
Take a look at `just upgrade-rust` recipe.
It bumps used version of Rust to the current stable version
(as [declared by manifest](https://static.rust-lang.org/dist/channel-rust-stable.toml)).

View File

@@ -2,7 +2,7 @@
address = "0.0.0.0"
port = 8080
[development]
[debug]
base-URI = "http://localhost:8080"
from = "noreply@localhost"
x-accel-redirect = false
@@ -36,7 +36,7 @@ maintenance_file = "maintenance"
enable_prometheus = false
email_template_dir = "email-templates"
[production]
[release]
base-URI = "https://keys.openpgp.org"
base-URI-Onion = "https://keys.openpgp.org"
from = "noreply@keys.openpgp.org"

1
aboutPages/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
public

20
aboutPages/config.toml Normal file
View File

@@ -0,0 +1,20 @@
# Zola configuration file
# https://www.getzola.org/documentation/getting-started/configuration/
title = "keys.openpgp.org"
# change with --base-url
base_url = "/about"
generate_feeds = true
feed_filenames = ["atom.xml"]
[extra]
menu = [
"_index.md",
"news/_index.md",
"usage.md",
"faq.md",
"stats.md",
"privacy.md",
]

View File

@@ -0,0 +1,27 @@
---
title: About
template: page.html
---
The {{ brand() }} server is a public service for the distribution and discovery of OpenPGP-compatible keys, commonly referred to as a "keyserver".
**For instructions, see our [usage guide](@/usage.md).**
### How it works
An OpenPGP key contains two types of information:
- **Identity information** describes the parts of a key that identify its owner, also known as "User IDs". A User ID typically includes a name and an email address.
- **Non-identity information** is all the technical information about the key itself. This includes the large numbers used for verifying signatures and encrypting messages. It also includes metadata like date of creation, some expiration dates, and revocation status.
Traditionally, these pieces of information have always been distributed together. On {{ brand() }}, they are treated differently. While anyone can upload all parts of any OpenPGP key to {{ brand() }}, our keyserver will only retain and publish certain parts under certain conditions:
Any **non-identity information** will be stored and freely redistributed, if it passes a cryptographic integrity check. Anyone can download these parts at any time as they contain only technical data that can't be used to directly identify a person. Good OpenPGP software can use {{ brand() }} to keep this information up to date for any key that it knows about. This helps OpenPGP users maintain secure and reliable communication.
The **identity information** in an OpenPGP key is only distributed with consent. It contains personal data, and is not strictly necessary for a key to be used for encryption or signature verification. Once the owner gives consent by verifying their email address, the key can be found via search by address.
### Community and platform {#community}
This service is run as a community effort. You can talk to us in #hagrid on OFTC IRC, also reachable as #hagrid:stratum0.org on Matrix. Of course you can also reach us via email, at <tt>support at keys dot openpgp dot org</tt>. The folks who are running this come from various projects in the OpenPGP ecosystem, including Sequoia-PGP, OpenKeychain, and Enigmail.
Technically, {{ brand() }} runs on the [Hagrid](https://gitlab.com/keys.openpgp.org/hagrid) keyserver software, which is based on [Sequoia-PGP](https://sequoia-pgp.org). We are running on [eclips.is](https://eclips.is), a hosting platform focused on Internet Freedom projects, which is managed by [Greenhost](https://greenhost.net/).

142
aboutPages/content/api.md Normal file
View File

@@ -0,0 +1,142 @@
---
title: API
---
Hagrid implements both the legacy HKP interface, as well as our native interface, VKS.
## Verifying Keyserver (VKS) Interface
Hagrid has its own URL scheme to fetch, submit, and verify keys.
* <tt>GET /vks/v1/by-fingerprint/\<FINGERPRINT></tt>
Retrieves the key with the given <tt>Fingerprint</tt>. The <tt>Fingerprint</tt> may refer to the primary key, or any subkey. Hexadecimal digits MUST be uppercase, and MUST NOT be prefixed with `0x`. The returned key is ASCII Armored, and has a content-type of `application/pgp-keys`.
* <tt>GET /vks/v1/by-keyid/\<KEY-ID></tt>
Retrieves the key with the given long <tt>KeyID</tt>. The <tt>KeyID</tt> may refer to the primary key, or any subkey. Hexadecimal digits MUST be uppercase, and MUST NOT be prefixed with `0x`. The returned key is ASCII Armored, and has a content-type of `application/pgp-keys`.
* <tt>GET /vks/v1/by-email/\<URI-ENCODED EMAIL-ADDRESS></tt>
Retrieves the key with the given <tt>Email Address</tt>. Only exact matches are accepted. Lookup by email address requires opt-in by the owner of the email address. The returned key is ASCII Armored, and has a content-type of `application/pgp-keys`.
* <tt>POST /vks/v1/upload</tt>
A single key may be submitted using a POST request to <tt>/vks/v1/upload</tt>. The body of the request must be `application/json`. The JSON data must contain a single field `keytext`, which must contain the keys to submit. The value of `keytext` can be formatted in standard OpenPGP ASCII Armor, or base64.
The returned JSON data contains the fields `token`, `key_fpr`, and `status`. The `token` field contains an opaque value, which can be used to perform <tt>request-verify</tt> requests (see below). The `key_fpr` field contains the fingerprint of the uploaded primary key. The `status` token contains a map of email addresses contained in the key, with one of the values `unpublished`, `published`, `revoked`, or `pending`, indicating the status of this email address.
<div class="example">
<div>
Example request:
<pre>
{
"keytext": "&lt;ASCII ARMORED KEY&gt;"
}
</pre>
</div>
<div>
Example response:
<pre>
{
"key_fpr": "&lt;FINGERPRINT&gt;",
"status": {
"address@example.org": "unpublished"
},
"token": "..."
}
</pre>
</div>
</div>
* <tt>POST /vks/v1/request-verify</tt>
A key that has been uploaded can be made discoverable by one or more of its email addresses by proving ownership of the address via a verification email. This endpoint requests verification for one or more email addresses.
The body of the request must be `application/json`. The JSON data must include the opaque `token` value (obtained via <tt>/vks/v1/upload</tt>) and an `addresses` field, which is a list of email addresses (not full User IDs) to request verification mails for. It can optionally include a `locale` field, which is list of locales, ordered by preference, which to use for the verification email. The reply will be the same as for the <tt>/vks/v1/upload</tt> endpoint, with addresses marked as `pending` where a verification email has been sent.
<div class="example">
<div>
Example request:
<pre>
{
"token": "...",
"addresses": [
"address@example.org"
],
"locale": [ "de_CH", "de_DE" ]
}
</pre>
</div>
<div>
Example response:
<pre>
{
"key_fpr": "&lt;FINGERPRINT&gt;",
"status": {
"address@example.org": "pending"
},
"token": "..."
}
</pre>
</div>
</div>
### Error handling
If a GET request fails for any reason, a suitable HTTP status code will be returned. The response will be a plaintext error message. If a key is not found, the HTTP status code will be <tt>404</tt>.
If a POST request fails for any reason, a suitable HTTP status code will be returned. The response body will be a JSON object with a single `error` attribute. A POST request may fail with a HTTP 503 error at any time if the server is undergoing database maintenance. **Clients should handle errors gracefully for POST requests.**
<div class="example">
<div>
Example response:
<pre>
{
"error": "We are currently undergoing scheduled database maintenance!"
}
</pre>
</div>
</div>
### [Rate Limiting](#rate-limiting)
Requests to the {{ brand() }} API are rate limited:
* Requests by fingerprint or key id are limited to five requests per second. Excessive requests will fail with <tt>error 429</tt>. There is a burst window of 1000.
* Requests by email address are limited to one request per minute. Excessive requests will fail with <tt>error 429</tt>. There is a burst window of 50.
## HTTP Keyserver Protocol (HKP) Interface
Hagrid implements a subset of the [HKP](https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00) protocol so that tools like GnuPG and OpenKeychain can use it without modification.
* <tt>GET /pks/lookup?op=get&options=mr&search=\<QUERY></tt>
Returns an ASCII Armored key matching the query. Query may be:
* An exact email address query of the form `localpart@example.org`.
* A hexadecimal representation of a long <tt>KeyID</tt> (e.g., `069C0C348DD82C19`, optionally prefixed by `0x`). This may be a <tt>KeyID</tt> of either a primary key or a subkey.
* A hexadecimal representation of a <tt>Fingerprint</tt> (e.g., `8E8C33FA4626337976D97978069C0C348DD82C19`, optionally prefixed by `0x`). This may be a <tt>Fingerprint</tt> of either a primary key or a subkey.
* <tt>GET /pks/lookup?op=index&options=mr&search=\<QUERY></tt>
Returns a [machine-readable list](https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00#section-5.2) of keys matching the query. Query may have the forms detailed above. Hagrid always returns either one or no keys at all.
* <tt>POST /pks/add</tt>
Keys may be submitted using a POST request to <tt>/pks/add</tt>, the body of the request being a `application/x-www-form-urlencoded` query. `keytext` must be the keys to submit, which must be ASCII Armored. More than one key may be submitted in one request. For verification of email addresses, the <tt>/vks/v1/upload</tt> endpoint must be used instead.
[Rate limiting](#rate-limiting) applies as for the VKS interface above.
#### Limitations
By design, Hagrid does not implement the full HKP protocol. The specific limitations are:
* No support for `op=vindex`.
* Only exact matches by email address, fingerprint or long key id are returned.
* All requests return either one or no keys.
* The expiration date field in `op=index` is left blank (discussion [here](https://gitlab.com/keys.openpgp.org/hagrid/issues/134)).
* All parameters and options other than `op` and `search` are ignored.
* Output is always machine readable (i.e. `options=mr` is always assumed).
* Uploads are restricted to 1 MiB.
* All packets that aren't public keys, user IDs or signatures are filtered out.

109
aboutPages/content/faq.md Normal file
View File

@@ -0,0 +1,109 @@
---
title: FAQ
---
**For instructions, see our [usage guide](@/usage.md).**
### Is this server part of the "SKS" pool? {#sks-pool}
No. The federation model of the SKS pool has various problems in terms of reliability, abuse-resistance, privacy, and usability. We might do something similar to it, but {{ brand() }} will never be part of the SKS pool itself.
### Is keys.openpgp.org federated? Can I help by running an instance? {#federation}
For the moment, no. We do plan to decentralize {{ brand() }} at some point. With multiple servers run by independent operators, we can hopefully improve the reliability of this service even further.
Several folks offered to help out by "running a Hagrid server instance". We very much appreciate the offer, but we will probably never have an "open" federation model like SKS, where everyone can run an instance and become part of a "pool". This is for two reasons:
1. Federation with open participation requires all data to be public. This significantly impacts the privacy of our users, because it allows anyone to scrape a list of all email addresses.
2. Servers run as a hobby by casual administrators do not meet our standards for reliability and performance.
### Why is there no support for identities that aren't email addresses? {#non-email-uids}
We require explicit consent to distribute identity information. Identities that aren't email addresses, such as pictures or website URLs, offer no simple way for us to acquire this consent.
Note: Some OpenPGP software creates keys with incorrectly formatted email addresses. These addresses might not be recognized correctly on {{ brand() }}.
### Can I verify more than one key for some email address? {#verify-multiple}
An email address can only be associated with a single key. When an address is verified for a new key, it will no longer appear in any key for which it was previously verified. [Non-identity information](@/_index.md) will still be distributed for all keys.
This means a search by email address will only return a single key, not multiple candidates. This eliminates an impossible choice for the user ("Which key is the right one?"), and makes key discovery by email much more convenient.
### What do you do to protect outgoing verification emails? {#email-protection}
We use a modern standard called [MTA-STS](https://www.hardenize.com/blog/mta-sts), combined with [STARTTLS Everywhere](https://starttls-everywhere.org/) by the EFF, to make sure verification emails are sent out securely. This protects against eavesdropping and interception during delivery.
The MTA-STS mechanism only works if supported by the recipient's email provider. Otherwise, emails will be delivered as usual. You can [run this test](https://www.hardenize.com/) to see if your email provider supports it. If the "MTA-STS" entry on the left isn't a green checkmark, please ask your provider to update their configuration.
### Do you distribute "third party signatures"? {#third-party-signatures}
Short answer: No.
A "third party signature" is a signature on a key that was made by some other key. Most commonly, those are the signatures produced when "signing someone's key", which are the basis for the "[Web of Trust](https://en.wikipedia.org/wiki/Web_of_trust)". For a number of reasons, those signatures are not currently distributed via {{ brand() }}.
The killer reason is **spam**. Third party signatures allow attaching arbitrary data to anyone's key, and nothing stops a malicious user from attaching so many megabytes of bloat to a key that it becomes practically unusable. Even worse, they could attach offensive or illegal content.
There are ideas to resolve this issue. For example, signatures could be distributed with the signer, rather than the signee. Alternatively, we could require cross-signing by the signee before distribution to support a [caff-style](https://wiki.debian.org/caff) workflow. If there is enough interest, we are open to working with other OpenPGP projects on a solution.
### Why not sign keys after verification? {#no-sign-verified}
The {{ brand() }} service is meant for key distribution and discovery, not as a de facto certification authority. Client implementations that want to offer verified communication should rely on their own trust model.
### Why are revoked identities not distributed as such? {#revoked-uids}
When an OpenPGP key marks one of its identities as revoked, this identity should no longer be considered valid for the key, and this information should ideally be distributed to all OpenPGP clients that already know about the newly revoked identity.
Unfortunately, there is currently no good way to distribute revocations, that doesn't also reveal the revoked identity itself. We don't want to distribute revoked identities, so we can't distribute the identity at all.
There are proposed solutions to this issue, that allow the distribution of revocations without also revealing the identity itself. But so far there is no final specification, or support in any OpenPGP software. We hope that a solution will be established in the near future, and will add support on {{ brand() }} as soon as we can.
### Why isn't it possible to search by part of an email address, like just the domain? {#search-substring}
Some keyservers support search for keys by part of an email address. This allows discovery not only of keys, but also of addresses, with a query like "keys for addresses at gmail dot com". This effectively puts the addresses of all keys on those keyservers into a public listing.
A search by email address on {{ brand() }} returns a key only if it exactly matches the email address. That way, a normal user can discover the key associated with any address they already know, but they cannot discover any new email addresses. This prevents a malicious user or spammer from easily obtaining a list of all email addresses on the server.
We made this restriction a part of our [privacy policy](@/privacy.md), which means we can't change it without asking for user consent.
### Do you support Tor? {#tor}
Of course! If you have Tor installed, you can reach {{ brand() }} anonymously as an [onion service](https://support.torproject.org/onionservices/#onionservices-2):
[zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion](http://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion)
### Why not encrypt verification emails? {#encrypt-verification-emails}
Various reasons:
1. It is more complicated, both for our users and for us.
2. It doesn't prevent attacks - an attacker gains nothing from uploading a key they don't have access to.
3. Deletion would still have to be possible even when a key is lost.
4. It would require a different (and more complicated) mechanism to upload keys that can only sign.
### I have trouble updating some keys with GnuPG. Is there a bug? {#older-gnupg}
GnuPG considers keys that contain no identity information to be invalid, and refuses to import them. However, a key that has no [verified email addresses](@/_index.md) may still contain useful information. In particular, it's still possible to check whether the key is revoked or not.
In June 2019, the {{ brand() }} team created a patch that allows GnuPG to process updates from keys without identity information. This patch was quickly included in several downstream distributions of GnuPG, including Debian, Fedora, NixOS, and GPG Suite for macOS.
In March 2020 the GnuPG team rejected the patch, and updated the issue status to "Wontfix". This means that **unpatched versions of GnuPG cannot receive updates from {{ brand() }} for keys that don't have any verified email address**. You can read about this decision in issue [T4393](https://dev.gnupg.org/T4393#133689) on the GnuPG bug tracker.
You can check if your version of GnuPG is affected with the following instructions.
> Import test key:
>
> $ curl https://keys.openpgp.org/assets/uid-test.pub.asc | gpg --import
> gpg: key F231550C4F47E38E: "Alice Lovelace <alice@openpgp.example>" imported
> gpg: Total number processed: 1
> gpg: imported: 1
> With patch, key will be updated if locally known:
>
> $ gpg --recv-keys EB85BB5FA33A75E15E944E63F231550C4F47E38E
> gpg: key F231550C4F47E38E: "Alice Lovelace <alice@openpgp.example>" not changed
> gpg: Total number processed: 1
> gpg: unchanged: 1
> Without patch, a key without identity is always rejected:
>
> $ gpg --recv-keys EB85BB5FA33A75E15E944E63F231550C4F47E38E
> gpg: key EB85BB5FA33A75E15E944E63F231550C4F47E38E: no user ID

View File

@@ -0,0 +1,45 @@
---
title: Launching a new keyserver! 🚀
---
From a community effort by [Enigmail](https://enigmail.net), [OpenKeychain](https://openkeychain.org), and [Sequoia PGP](https://sequoia-pgp.org), we are pleased to announce the launch of the new public OpenPGP keyserver {{ brand() }}! Hurray! 🎉
#### Give me the short story!
* Fast and reliable. No wait times, no downtimes, no inconsistencies.
* Precise. Searches return only a single key, which allows for easy key discovery.
* Validating. Identities are only published with consent, while non-identity information is freely distributed.
* Deletable. Users can delete personal information with a simple email confirmation.
* Built on Rust, powered by [Sequoia PGP](https://sequoia-pgp.org) - free and open source, running AGPLv3.
Get started right now by [uploading your key](/upload)!
#### Why a new keyserver?
We created {{ brand() }} to provide an alternative to the SKS Keyserver pool, which is the default in many applications today. This distributed network of keyservers has been struggling with [abuse](https://medium.com/@mdrahony/are-sks-keyservers-safe-do-we-need-them-7056b495101c), [performance](https://en.wikipedia.org/wiki/Key_server_\(cryptographic\)#Problems_with_keyservers), as well as [privacy issues](http://www.openwall.com/lists/oss-security/2017/12/10/1), and more recently also [GDPR](http://nongnu.13855.n7.nabble.com/SKS-apocalypse-mitigation-td228252.html) compliance questions. Kristian Fiskerstrand has done a stellar job maintaining the pool for [more than ten years](https://blog.sumptuouscapital.com/2016/12/10-year-anniversary-for-sks-keyservers-net/), but at this point development activity seems to have [mostly ceased](https://bitbucket.org/skskeyserver/sks-keyserver/pull-requests/60/clean-build-with-405).
We thought it time to consider a fresh approach to solve these problems.
#### Identity and non-identity information
The {{ brand() }} keyserver splits up identity and non-identity information in keys. You can find more details on our [about page](@/_index.md): The gist is that non-identity information (keys, revocations, and so on) is freely distributed, while identity information is only distributed with consent that can also be revoked at any time.
If a new key is verified for some email address, it will replace the previous one. This way, every email address is only associated with a single key at most. It can also be removed from the listing at any time by the owner of the address. This is very useful for key discovery: if a search by email address returns a key, it means this is the single key that is currently valid for the searched email address.
#### Support in Enigmail and OpenKeychain
The {{ brand() }} keysever will receive first-party support in upcoming releases of [Enigmail](https://enigmail.net) for Thunderbird, as well as [OpenKeychain](https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain&hl=en) on Android. This means users of those implementations will benefit from the faster response times, and improved key discovery by email address. We hope that this will also give us some momentum to build this project into a bigger community effort.
#### Current challenges
Privacy-preserving techniques in keyservers are still new, and sadly there are still a few compatibility issues caused by splitting out identity information.
In particular, when GnuPG (as of this writing, version 2.2.16) encounters an OpenPGP key without identities, it throws an error "no user ID" and does not process new non-identity information (like revocation certificates) even if it is cryptographically valid. We are actively engaged in providing fixes for these issues.
#### The future
Privacy-preserving techniques in keyservers are still new, and we have more ideas for reducing the metadata. But for now, our plan is only to keep {{ brand() }} reliable and fast 🐇, fix any upcoming bugs 🐞, and [listen to feedback](/about#community) from the community. 👂
For more info, head on over to our [about page](@/_index.md) and [FAQ](@/faq.md) pages. You can get started right away by [uploading your your key](/upload)! Beyond that there is more cool stuff to discover, like our [API](@/api.md), and an [Onion Service](@/faq.md#tor)!
Cheers! 🍻

View File

@@ -0,0 +1,50 @@
---
title: Three months after launch ✨
---
It has been three months now [since we launched](/news#2019-06-12-launch) {{ brand() }}. We are happy to report: It has been a resounding success! 🥳
#### Adoption in clients
The {{ brand() }} keyserver has been received very well by users, and clients are adopting it rapidly. It is now used by default in [GPGTools](https://gpgtools.org/), [Enigmail](https://enigmail.net/), [OpenKeychain](https://www.openkeychain.org/), [GPGSync](https://github.com/firstlookmedia/gpgsync), Debian, NixOS, and others. Many tutorials have also been updated, pointing users our way.
At the time of writing, more than 70.000 email addresses have been verified.
![](stats-addresses-2019-09-12.png)\
If that isn't a promising curve, I don't know what is :)
A special shout-out here goes to GPGTools for macOS. They implemented the update process so smoothly, the number of verified addresses completely exploded when they released their update.
#### All's good in operations
There is not a lot to report operationally, and no news is good news in this case! Since launch, there was nearly zero downtime, only a single bug came up that briefly caused issues during upload, and support volume has been comfortably low.
Our traffic is currently at about ten requests per second (more during the day, less on the weekend), and we delivered roughly 100.000 emails in the last month. No sweat.
We made several small operational improvements including deployment of [DNSSEC](http://dnsviz.net/d/keys.openpgp.org/dnssec/), implementing some [rate-limiting](/api#rate-limiting), nailing down our [content security policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) headers, and enabling [single-hop](https://blog.torproject.org/whats-new-tor-0298) mode on our Tor Onion Service. You can find a more complete list [here](https://gitlab.com/keys.openpgp.org/hagrid/merge_requests?scope=all&utf8=%E2%9C%93&state=merged).
#### Secure email delivery with MTA-STS
One improvement that deserves special mention is [MTA-STS](https://www.hardenize.com/blog/mta-sts), which improves the security of outgoing emails.
While HTTPS is deployed fairly universally these days, that sadly isn't the case for email. Many servers don't do encryption at all, or use a self-signed certificate instead of a proper one (e.g. from Let's Encrypt). But delivery failures upset customers more than reduced security, and many emails are still delivered without encryption.
With MTA-STS, domain operators can indicate (via HTTPS) that their email server _does_ support encryption. When a secure connection can't be established to such a server, message delivery will be postponed or eventually bounce, instead of proceeding insecurely.
This is extremely useful for service like {{ brand() }}. If encryption isn't reliable, attackers can intercept verification emails relatively easily. But for providers who have MTA-STS deployed, we can be sure that every message is delivered securely, and to the right server.
You can [run a check](https://aykevl.nl/apps/mta-sts/) to find out whether your email provider supports MTA-STS. If they don't, please drop them a message and tell them to step up their security game!
#### Work in progress
We are working on two features:
The first is **localization**. Most people do not speak English, but so far that is the only language we support. To make this service more accessible, we are working with the OTF's [Localization Lab](https://www.opentech.fund/labs/localization-lab/) to make the website and outgoing emails available in several more languages.
The second is to bring back **third-party signatures**. As [mentioned in our FAQ](@/faq.md#third-party-signatures), we currently don't support these due to spam and potential for abuse. The idea is to require [cross-signatures](https://gitlab.com/openpgp-wg/rfc4880bis/merge_requests/20/diffs), which allow each key to choose for itself which signatures from other people it wants to distribute. Despite this extra step, this is fairly compatible with existing software. It also nicely stays out of the way of users who don't care about signatures.
Although work is in progress for both of those features, neither have a planned time of release yet.
Regarding the "no user ID" issue with GnuPG (mentioned in our [last news post](/news#2019-06-12-launch-challenges) and our [FAQ](@/faq.md#older-gnupg)), a patch that fixes this problem is now carried by Debian, as well as GPGTools for macOS. GnuPG upstream has not merged the patch so far.
That's it! Thanks for your interest! 👋

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,20 @@
---
title: Celebrating 100.000 verified addresses! 📈
---
Five months ago, we launched this service. And just today, we have reached a remarkable milestone:
![](stats-addresses-2019-11-12.png)\
**One hundred thousand verified email addresses!**
Thanks to everyone using this service! And thanks especially to those who have provided us with feedback, translations, or even code contributions!
A few updates on things we've been working on:
* This news page is now available as an **[atom feed {{ img_height(src="/img/atom.svg", height="0.8em") }}](/atom.xml)**.
* We have been working on a **[new mechanism to refresh keys](https://gitlab.com/keys.openpgp.org/hagrid/issues/131)** that better protects the user's privacy.
* Work on **localization** is in full swing! we hope to have some languages ready for deployment soon.
If you would like to see {{ brand() }} translated into your native language, please [join the translation team](https://www.transifex.com/otf/hagrid/) over on Transifex. We would appreciate help especially for **Russian**, **Italian**, **Polish** and **Dutch**.
That's all, keeping this one short! 👍️

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,23 @@
---
title: keys.openpgp.org governance 📜
---
It's been quite a while since the last update. Not a lot happened around {{ brand() }} during this time, operationally. 😴
But no news is good news in this case: A few bugs were fixed, some software maintenance was perfomed to keep up with the ecosystem. There were no significant outages, we've had some steady growth of users, things are generally working as expected. Hurray!
There is, however, an important bit of news: {{ brand() }} has a governance process now. In particular, there is now a written constitution for the service, which you can find [here](https://gitlab.com/keys.openpgp.org/governance/-/blob/main/constitution.md).
Most importantly, there is now a board, who were elected by a community of contributors to the OpenPGP ecosystem. This board currently consists of:
* Daniel Huigens, from Proton
* Lukas Pitschl, from GPGTools
* Neal Walfield, from Sequoia-PGP
* Ola Bini
* Vincent Breitmoser
The primary responsibility of the board is to make decisions on the future of {{ brand() }}. Which features should go in, which not? We are having regular meetings at the moment, and progress is slow but steady. We'll be sure to let you know (via this news blog) when anything exciting happens!
You can find more info about governance in the [repository](https://gitlab.com/keys.openpgp.org/governance/). You can also reach the board via email at <tt>board at keys.openpgp.org</tt>.
That's all for now! 🙇

View File

@@ -0,0 +1,5 @@
---
title: News
sort_by: date
template: news.html
---

View File

@@ -0,0 +1,59 @@
---
title: Privacy
---
### Name and contact details
{{ brand() }} is a community effort. You can find more information about us, and our contact, details [here](@/_index.md).
### How we process data
The public keyserver running on {{ brand() }} processes, stores, and distributes OpenPGP certificate data. The specific way in which data is processed differs by type as follows:
* #### Email Addresses
Email addresses of individuals contained in User IDs are personal data. Special care is taken to make sure they are used only with consent, which you can withdraw at any time:
* Publishing requires double opt-in validation, to prove ownership of the email address in question.
* Addresses are searchable by exact email address, but not by associated name.
* Enumeration of addresses is not possible.
* Deletion of addresses is possible via simple proof of ownership in an automated fashion, similar to publication, using the ["manage" tool](/manage). To unlist an address where this isn't possible, write to support at keys dot openpgp dot org.
This data is never handed collectively ("as a dump") to third parties.
* #### Public Key Data
We process the cryptographic content of OpenPGP certificates - such as public key material, self-signatures, and revocation signatures for the legitimate interest of providing the service.
This data is not usually collectively available ("as a dump"), but may be handed upon request to third parties for purposes of development or research.
If you upload your OpenPGP certificates to the service, you are the source of this data. It is also possible for anyone who has your public OpenPGP certificate to upload them to this service for example, if you have published them somewhere else, or sent them to someone. This does not include publication of Email Addresses, which are only used with explicit consent as described above.
* #### Other User ID data
An OpenPGP certificate may contain personal data other than email addresses, such as User IDs that do not contain email addresses, or image attributes. This data is stripped during upload and never stored, processed, or distributed in any way.
OpenPGP packet types that were not specifically mentioned above are stripped during upload and never stored, processed or distributed in any way.
Data is never relayed to third parties outside of what is available from the public API interfaces, and what is described in this policy and on our [about page](@/_index.md).
This service is available on the Internet, so anyone, anywhere in the world, can access it and retrieve data from it.
### Retention periods
We will retain your email address linked with your OpenPGP certificates until you remove it. We will remove your Public Key Data if you wish, but note that anyone can re-upload it to the service, in keeping with the "public" nature of this key material.
All incoming requests are logged for a period of 30 days, and only used as necessary for operation of the service. IP addresses are anonymized for storage.
### Your rights
You can withdraw consent to the processing of your email address at any time, or erase your email addresses, using the ["manage" tool](/manage).
You can obtain access to the personal data we process about you by viewing your OpenPGP certificates, or searching for your certificates using your email addresses, using this service.
You can delete your OpenPGP certificates by emailing support at keys dot openpgp dot org, but note that anyone can upload them again. If you object to having your certificate re-uploaded, email support at keys dot openpgp dot org and we will banlist your keys.
To exercise the right of portability, you can download your OpenPGP certificate using this service.
If you are in the EEA or UK, you also have the right to lodge a complaint with a supervisory authority, such as your local data protection authority.

View File

@@ -0,0 +1,11 @@
---
title: Stats
---
### Verified email addresses
A simple statistic of the total number of email addresses that are currently verified. 📈
![](/about/stats/month.png)
![](/about/stats/year.png)

125
aboutPages/content/usage.md Normal file
View File

@@ -0,0 +1,125 @@
---
title: Usage
---
On this page, we collect information on how to use {{ brand() }} with different OpenPGP software products.
We are still in the process of adding more. If you are missing some, please write to us and we'll try to add it.
## Web Interface {#web}
The web interface on {{ brand() }} allows you to:
* [Search](/) for keys manually, by fingerprint or email address.
* [Upload](/upload) keys manually, and verify them after upload.
* [Manage](/manage) your keys, and remove published identities.
## {{ image(path="/img/enigmail.svg",alt="") }} Enigmail {#enigmail}
[Enigmail](https://enigmail.net) for Thunderbird uses {{ brand() }} by default since version 2.0.12.
Full support is available since Enigmail 2.1 (for [Thunderbird 68](https://www.thunderbird.net/en-US/thunderbird/68.0beta/releasenotes/) or newer):
* Keys will be kept up to date automatically.
* During key creation, you can upload and verify your key.
* Keys can be discovered by email address.
## {{ image(path="/img/gpgtools.png",alt="") }} GPG Suite {#gpg-suite}
[GPG Suite](https://gpgtools.org/) for macOS uses {{ brand() }} by default since August 2019.
## {{ image(path="/img/openkeychain.svg",alt="") }} OpenKeychain {#openkeychain}
[OpenKeychain](https://www.openkeychain.org/) for Android uses {{ brand() }} by default since July 2019.
* Keys will be kept up to date automatically.
* Keys can be discovered by email address.
Note that there is no built-in support for upload and email address verification so far.
## {{ image(path="/img/pignus.png",alt="") }} Pignus {#pignus}
[Pignus](https://www.frobese.de/pignus/) for iOS uses {{ brand() }} by default since November 2019.
* Your keys can be uploaded at any time.
* Keys can be discovered by email address.
## {{ image(path="/img/gnupg.svg",alt="") }} GnuPG {#gnupg}
To configure [GnuPG](https://gnupg.org) to use {{ brand() }} as keyserver, add this line to your gpg.conf file:
> keyserver hkps://keys.openpgp.org
#### Retrieving keys {#gnupg-retrieve}
* To locate the key of a user, by email address:
> gpg --auto-key-locate keyserver --locate-keys user@example.net
* To refresh all your keys (e.g. new revocation certificates and subkeys):
> gpg --refresh-keys
#### Uploading your key {#gnupg-upload}
Keys can be uploaded with GnuPG's --send-keys command, but identity information can't be verified that way to make the key searchable by email address ([what does this mean?](@/_index.md)).
* You can try this shortcut for uploading your key, which outputs a direct link to the verification page:
> gpg --export your\_address@example.net | curl -T - http://localhost:8080
* Alternatively, you can export them to a file and select that file in the [upload](/upload) page:
> gpg --export your\_address@example.net > my\_key.pub
#### Troubleshooting {#gnupg-troubleshooting}
* Some old \~/gnupg/dirmngr.conf files contain a line like this:
> hkp-cacert ~/.gnupg/sks-keyservers.netCA.pem
This configuration is no longer necessary, but prevents regular certificates from working. It is recommended to simply remove this line from the configuration.
* While refreshing keys, you may see errors like the following:
> gpg: key A2604867523C7ED8: no user ID
This is a [known problem in GnuPG](https://dev.gnupg.org/T4393). We are working with the GnuPG team to resolve this issue.
#### Usage via Tor {#gnupg-tor}
For users who want to be extra careful, {{ brand() }} can be reached anonymously as an [onion service](https://support.torproject.org/onionservices/#onionservices-2). If you have [Tor](https://www.torproject.org/) installed, use the following configuration:
> keyserver hkp://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion
## WKD as a Service {#wkd-as-a-service}
The Web Key Directory (WKD) is a standard for discovery of OpenPGP keys by email address, via the domain of its email provider. It is used to discover unknown keys in some email clients, such as [GpgOL](https://www.gpg4win.de/about.html).
{{ brand() }} can be used as a managed WKD service for any domain. To do so, the domain simply needs a CNAME record that delegates its openpgpkey subdomain to wkd.keys.openpgp.org. It should be possible to do this in the web interface of any DNS hoster.
Once enabled for a domain, its verified addresses will automatically be available for lookup via WKD.
The CNAME record should look like this:
> $ drill openpgpkey.example.org
> ...
> openpgpkey.example.org. 300 IN CNAME wkd.keys.openpgp.org.
There is a simple status checker for testing the service:
> $ curl 'https://wkd.keys.openpgp.org/status/?domain=openpgpkey.example.org'
> CNAME lookup ok: openpgpkey.example.org resolves to wkd.keys.openpgp.org
For testing key retrieval:
> $ gpg --locate-keys --auto-key-locate clear,nodefault,wkd address@example.org
## API
We offer an API for integrated support in OpenPGP applications. Check out our [API documentation](@/api.md).
## Others
Missing a guide for your favorite implementation? This site is a work-in-progress, and we are looking to improve it. Drop us a line at support at keys dot openpgp dot org if you want to help out!

26
aboutPages/default.nix Normal file
View File

@@ -0,0 +1,26 @@
{ lib, stdenvNoCC, zola, commitShaShort ? "" }:
stdenvNoCC.mkDerivation rec {
pname = "hagridAboutPages";
version = "2.1.0";
src = ./.;
nativeBuildInputs = [
zola
];
buildPhase = ''
zola build --output-dir $out/about
'';
COMMIT_SHA_SHORT = commitShaShort;
meta = with lib; {
description = "A verifying keyserver (about pages)";
homepage = "https://gitlab.com/keys.openpgp.org/hagrid";
license = with licenses; [ gpl3 ];
maintainers = with maintainers; [ valodim ];
platforms = platforms.all;
};
}

View File

@@ -0,0 +1,305 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<defs >
<font id="Roboto" horiz-adv-x="1176" ><font-face
font-family="Roboto Medium"
units-per-em="2048"
panose-1="2 0 0 0 0 0 0 0 0 0"
ascent="1900"
descent="-500"
alphabetic="0" />
<glyph unicode=" " horiz-adv-x="510" />
<glyph unicode="!" horiz-adv-x="549" d="M382 429H173L150 1456H406L382 429ZM143 115Q143 172 180 209T281 247T382 210T419 115Q419 60 383 23T281 -14T179 23T143 115Z" />
<glyph unicode="&quot;" horiz-adv-x="664" d="M275 1399L240 1012H101V1536H275V1399ZM576 1399L541 1012H402V1536H576V1399Z" />
<glyph unicode="#" horiz-adv-x="1250" d="M719 410H495L419 0H251L327 410H96V568H357L415 881H172V1040H445L523 1456H690L612 1040H837L915 1456H1082L1004 1040H1212V881H974L916 568H1137V410H886L810 0H643L719 410ZM525 568H749L807 881H583L525 568Z" />
<glyph unicode="$" horiz-adv-x="1164" d="M819 380Q819 465 765 520T585 620T389 703Q156 828 156 1073Q156 1239 257 1346T531 1473V1691H691V1471Q865 1446 960 1324T1055 1005H813Q813 1131 757 1203T603 1276Q507 1276 453 1224T399 1075Q399 988 452 936T634
836T835 749T958 658T1035 539T1062 382Q1062 213 959 108T670 -16V-211H511V-17Q313 5 207 125T100 443H343Q343 317 406 248T586 179Q700 179 759 234T819 380Z" />
<glyph unicode="%" horiz-adv-x="1504" d="M99 1176Q99 1308 184 1392T407 1477Q547 1477 631 1393T716 1171V1099Q716 968 632 884T409 800Q274 800 187 882T99 1105V1176ZM269 1099Q269 1030 307 988T409 945Q471 945 509 987T547 1103V1176Q547 1245 509 1288T407
1331T307 1288T269 1173V1099ZM799 357Q799 491 886 574T1108 657Q1244 657 1330 574T1417 350V279Q1417 149 1334 65T1110 -20T885 63T799 284V357ZM969 279Q969 211 1008 168T1110 124Q1174 124 1210 165T1247 282V357Q1247 427 1208 469T1108 511Q1046 511 1008
469T969 353V279ZM459 109L334 181L1045 1319L1170 1247L459 109Z" />
<glyph unicode="&amp;" horiz-adv-x="1309" d="M86 393Q86 494 141 578T358 779Q273 886 240 961T206 1106Q206 1277 310 1376T590 1476Q749 1476 850 1383T952 1151Q952 1060 906 984T755 831L656 759L937 427Q998 547 998 694H1209Q1209 425 1083 253L1297 0H1015L933
97Q777 -20 561 -20T216 94T86 393ZM568 174Q691 174 798 256L480 631L449 609Q329 518 329 401Q329 300 394 237T568 174ZM434 1112Q434 1028 537 901L648 977L679 1002Q741 1057 741 1143Q741 1200 698 1240T589 1281Q518 1281 476 1233T434 1112Z" />
<glyph unicode="&apos;" horiz-adv-x="346" d="M267 1411L241 1020H82V1536H267V1411Z" />
<glyph unicode="(" horiz-adv-x="714" d="M128 592Q128 823 190 1030T372 1401T626 1631L674 1489Q533 1382 446 1163T350 660L349 574Q349 271 434 34T674 -328L626 -463Q492 -397 372 -233T190 138T128 592Z" />
<glyph unicode=")" horiz-adv-x="722" d="M593 576Q593 354 532 148T347 -228T88 -463L40 -328Q190 -212 277 26T365 571V594Q365 872 289 1100T71 1467L40 1495L88 1631Q216 1569 336 1411T520 1058T592 654L593 576Z" />
<glyph unicode="*" horiz-adv-x="905" d="M332 972L27 1060L82 1229L384 1112L369 1456H548L533 1106L830 1221L884 1049L574 961L774 695L629 589L449 877L271 598L125 700L332 972Z" />
<glyph unicode="+" horiz-adv-x="1141" d="M686 801H1066V579H686V146H450V579H68V801H450V1206H686V801Z" />
<glyph unicode="," horiz-adv-x="450" d="M159 -328L28 -250Q86 -159 107 -92T130 46V235H349L348 60Q347 -46 295 -152T159 -328Z" />
<glyph unicode="-" horiz-adv-x="672" d="M596 521H71V717H596V521Z" />
<glyph unicode="." horiz-adv-x="572" d="M276 256Q344 256 381 218T418 121Q418 64 381 27T276 -11Q211 -11 173 26T135 121T172 217T276 256Z" />
<glyph unicode="/" horiz-adv-x="810" d="M193 -125H2L575 1456H766L193 -125Z" />
<glyph unicode="0" horiz-adv-x="1164" d="M1058 613Q1058 299 941 140T583 -20Q347 -20 228 135T105 596V848Q105 1162 222 1319T581 1476Q820 1476 937 1323T1058 865V613ZM815 885Q815 1090 759 1185T581 1281Q462 1281 406 1191T347 908V578Q347 374 404 274T583
174Q700 174 756 266T815 556V885Z" />
<glyph unicode="1" horiz-adv-x="1164" d="M767 0H525V1169L168 1047V1252L736 1461H767V0Z" />
<glyph unicode="2" horiz-adv-x="1164" d="M1088 0H109V167L594 696Q699 813 743 891T788 1049Q788 1153 730 1217T572 1281Q454 1281 389 1209T324 1012H81Q81 1145 141 1251T314 1417T574 1476Q786 1476 908 1370T1031 1075Q1031 966 970 847T768 575L412 194H1088V0Z" />
<glyph unicode="3" horiz-adv-x="1164" d="M390 839H538Q650 840 715 897T781 1062Q781 1166 727 1223T560 1281Q462 1281 399 1225T336 1077H93Q93 1189 152 1281T318 1424T557 1476Q775 1476 899 1367T1024 1062Q1024 964 962 878T800 747Q920 706 982 618T1045
408Q1045 212 911 96T557 -20Q347 -20 213 92T79 390H322Q322 294 386 234T560 174Q673 174 738 234T803 408Q803 523 735 585T533 647H390V839Z" />
<glyph unicode="4" horiz-adv-x="1164" d="M931 519H1112V324H931V0H688V324H59L52 472L680 1456H931V519ZM307 519H688V1127L670 1095L307 519Z" />
<glyph unicode="5" horiz-adv-x="1164" d="M174 722L253 1456H1035V1246H455L415 898Q516 956 643 956Q851 956 966 823T1082 465Q1082 243 954 112T603 -20Q403 -20 272 93T129 393H364Q378 287 440 231T602 174Q714 174 776 254T839 472Q839 605 770 682T580
760Q514 760 468 743T368 674L174 722Z" />
<glyph unicode="6" horiz-adv-x="1164" d="M865 1463V1262H835Q631 1259 509 1150T364 841Q481 964 663 964Q856 964 967 828T1079 477Q1079 255 949 118T606 -20Q388 -20 253 141T117 563V646Q117 1029 303 1246T840 1463H865ZM604 768Q524 768 458 723T360 603V529Q360
367 428 272T604 176T775 257T838 470T774 685T604 768Z" />
<glyph unicode="7" horiz-adv-x="1164" d="M1078 1321L496 0H241L822 1261H69V1456H1078V1321Z" />
<glyph unicode="8" horiz-adv-x="1164" d="M1026 1072Q1026 965 971 882T821 750Q935 697 996 605T1058 397Q1058 205 928 93T582 -20Q365 -20 235 93T104 397Q104 514 166 607T340 750Q246 798 192 881T137 1072Q137 1258 257 1367T581 1476Q786 1476 906 1367T1026
1072ZM815 409Q815 517 751 583T580 650T411 584T347 409Q347 302 409 238T582 174T753 236T815 409ZM784 1063Q784 1158 729 1219T581 1281T434 1223T380 1063Q380 963 434 904T582 845T729 904T784 1063Z" />
<glyph unicode="9" horiz-adv-x="1164" d="M798 609Q676 480 513 480Q321 480 207 614T93 968Q93 1112 151 1229T316 1411T564 1476Q784 1476 913 1312T1042 873V805Q1042 411 864 204T333 -6H304V195H339Q554 198 669 298T798 609ZM564 670Q637 670 701 712T800
828V923Q800 1084 734 1182T563 1280T396 1194T333 975Q333 838 396 754T564 670Z" />
<glyph unicode=":" horiz-adv-x="543" d="M527 256Q595 256 632 218T669 121Q669 64 632 27T527 -11Q462 -11 424 26T386 121T423 217T527 256ZM271 1105Q339 1105 376 1067T413 970Q413 913 376 876T271 838Q206 838 168 875T130 970T167 1066T271 1105Z" />
<glyph unicode=";" horiz-adv-x="487" d="M250 1105Q318 1105 355 1067T392 970Q392 913 355 876T250 838Q185 838 147 875T109 970T146 1066T250 1105ZM177 -328L46 -250Q104 -159 125 -92T148 46V235H367L366 60Q365 -46 313 -152T177 -328Z" />
<glyph unicode="&lt;" horiz-adv-x="1041" d="M310 631L900 407V164L63 537V730L900 1102V859L310 631Z" />
<glyph unicode="=" horiz-adv-x="1146" d="M1007 780H145V982H1007V780ZM1007 356H145V557H1007V356Z" />
<glyph unicode="&gt;" horiz-adv-x="1066" d="M746 636L128 863V1102L992 730V537L128 165V404L746 636Z" />
<glyph unicode="?" horiz-adv-x="996" d="M350 428Q350 561 383 640T513 813T637 948Q677 1009 677 1080Q677 1174 631 1223T494 1273Q408 1273 356 1225T303 1093H60Q62 1270 180 1373T494 1476Q695 1476 807 1374T920 1089Q920 926 768 768L645 647Q579 572
577 428H350ZM333 117Q333 176 370 212T470 249Q534 249 571 212T608 117Q608 62 572 25T470 -12T369 25T333 117Z" />
<glyph unicode="@" horiz-adv-x="1832" d="M1741 518Q1729 268 1618 124T1317 -21Q1136 -21 1075 133Q1024 57 957 19T815 -19Q669 -19 594 101T536 422Q552 585 615 716T776 918T984 990Q1068 990 1132 969T1284 882L1232 319Q1213 121 1346 121Q1448 121 1513
230T1585 514Q1602 883 1443 1079T963 1275Q767 1275 616 1177T375 894T277 471Q265 230 334 56T547 -210T898 -301Q982 -301 1073 -281T1229 -227L1267 -364Q1206 -404 1103 -428T894 -453Q640 -453 458 -346T185 -34Q91 177 102 471Q114 745 225 963T528 1303T967
1424Q1216 1424 1395 1315T1664 1000T1741 518ZM732 422Q719 286 756 216T874 145Q928 145 976 192T1054 323L1099 816Q1049 835 1002 835Q891 835 821 731T732 422Z" />
<glyph unicode="A" horiz-adv-x="1363" d="M963 339H399L281 0H18L568 1456H795L1346 0H1082L963 339ZM470 543H892L681 1147L470 543Z" />
<glyph unicode="B" horiz-adv-x="1292" d="M148 0V1456H647Q894 1456 1023 1357T1152 1062Q1152 962 1098 882T940 758Q1058 726 1122 638T1187 425Q1187 220 1056 110T679 0H148ZM401 657V202H682Q801 202 868 261T935 425Q935 652 703 657H401ZM401 843H649Q767
843 833 896T900 1048Q900 1156 839 1204T647 1252H401V843Z" />
<glyph unicode="C" horiz-adv-x="1337" d="M1259 474Q1237 241 1087 111T688 -20Q514 -20 382 62T177 297T102 650V786Q102 992 175 1149T384 1391T700 1476Q941 1476 1088 1345T1259 975H1007Q989 1132 916 1201T700 1271Q535 1271 447 1151T356 797V668Q356
432 440 308T688 184Q837 184 912 251T1007 474H1259Z" />
<glyph unicode="D" horiz-adv-x="1338" d="M148 0V1456H578Q771 1456 920 1370T1152 1126T1234 764V691Q1234 484 1152 327T917 85T567 0H148ZM401 1252V202H566Q765 202 871 326T980 684V765Q980 1002 877 1127T578 1252H401Z" />
<glyph unicode="E" horiz-adv-x="1158" d="M999 650H401V202H1100V0H148V1456H1093V1252H401V850H999V650Z" />
<glyph unicode="F" horiz-adv-x="1125" d="M987 617H401V0H148V1456H1073V1252H401V819H987V617Z" />
<glyph unicode="G" horiz-adv-x="1394" d="M1264 189Q1185 86 1045 33T727 -20Q544 -20 403 63T186 300T106 661V775Q106 1105 264 1290T705 1476Q948 1476 1091 1356T1263 1010H1015Q973 1273 710 1273Q540 1273 452 1151T360 791V679Q360 443 459 313T736 182Q930
182 1012 270V555H712V747H1264V189Z" />
<glyph unicode="H" horiz-adv-x="1455" d="M1304 0H1052V647H401V0H148V1456H401V850H1052V1456H1304V0Z" />
<glyph unicode="I" horiz-adv-x="578" d="M415 0H163V1456H415V0Z" />
<glyph unicode="J" horiz-adv-x="1137" d="M744 1456H996V435Q996 226 866 103T521 -20Q293 -20 169 95T45 415H297Q297 299 354 241T521 182Q623 182 683 249T744 436V1456Z" />
<glyph unicode="K" horiz-adv-x="1291" d="M566 629L401 454V0H148V1456H401V773L541 946L967 1456H1273L732 811L1304 0H1004L566 629Z" />
<glyph unicode="L" horiz-adv-x="1108" d="M401 202H1062V0H148V1456H401V202Z" />
<glyph unicode="M" horiz-adv-x="1793" d="M476 1456L896 340L1315 1456H1642V0H1390V480L1415 1122L985 0H804L375 1121L400 480V0H148V1456H476Z" />
<glyph unicode="N" horiz-adv-x="1454" d="M1303 0H1050L401 1033V0H148V1456H401L1052 419V1456H1303V0Z" />
<glyph unicode="O" horiz-adv-x="1414" d="M1310 690Q1310 476 1236 315T1025 67T708 -20Q531 -20 393 66T179 313T102 682V764Q102 977 177 1140T390 1389T706 1476T1021 1391T1234 1145T1310 771V690ZM1057 766Q1057 1008 966 1137T706 1266Q542 1266 450 1138T355
774V690Q355 450 448 319T708 188Q876 188 966 316T1057 690V766Z" />
<glyph unicode="P" horiz-adv-x="1309" d="M401 541V0H148V1456H705Q949 1456 1092 1329T1236 993Q1236 779 1096 660T702 541H401ZM401 744H705Q840 744 911 807T982 991Q982 1109 910 1179T712 1252H401V744Z" />
<glyph unicode="Q" horiz-adv-x="1414" d="M1305 690Q1305 483 1240 332T1056 91L1306 -104L1142 -252L832 -7Q771 -20 701 -20Q525 -20 387 66T173 313T96 682V764Q96 977 171 1140T384 1389T699 1476Q879 1476 1016 1391T1229 1145T1305 771V690ZM1051 766Q1051
1012 959 1139T699 1266Q536 1266 444 1138T349 775V690Q349 454 441 321T701 188Q870 188 960 316T1051 690V766Z" />
<glyph unicode="R" horiz-adv-x="1278" d="M683 561H401V0H148V1456H660Q912 1456 1049 1343T1186 1016Q1186 870 1116 772T919 620L1246 13V0H975L683 561ZM401 764H661Q789 764 861 828T933 1005Q933 1122 867 1186T668 1252H401V764Z" />
<glyph unicode="S" horiz-adv-x="1236" d="M909 375Q909 471 842 523T598 628T318 746Q119 871 119 1072Q119 1248 262 1362T635 1476Q787 1476 906 1420T1093 1261T1161 1031H909Q909 1145 838 1209T633 1274Q509 1274 441 1221T372 1073Q372 993 446 940T690
836T963 721T1114 573T1162 377Q1162 195 1023 88T644 -20Q486 -20 354 38T148 200T74 440H327Q327 316 409 248T644 180Q776 180 842 233T909 375Z" />
<glyph unicode="T" horiz-adv-x="1243" d="M1200 1252H746V0H495V1252H45V1456H1200V1252Z" />
<glyph unicode="U" horiz-adv-x="1335" d="M1213 1456V483Q1213 251 1065 116T669 -20Q419 -20 272 113T125 484V1456H377V482Q377 336 451 259T669 182Q961 182 961 490V1456H1213Z" />
<glyph unicode="V" horiz-adv-x="1325" d="M661 317L1031 1456H1309L785 0H540L18 1456H295L661 317Z" />
<glyph unicode="W" horiz-adv-x="1802" d="M1290 360L1514 1456H1765L1429 0H1187L910 1063L627 0H384L48 1456H299L525 362L803 1456H1015L1290 360Z" />
<glyph unicode="X" horiz-adv-x="1296" d="M649 930L955 1456H1247L807 734L1257 0H962L649 534L335 0H41L492 734L51 1456H343L649 930Z" />
<glyph unicode="Y" horiz-adv-x="1248" d="M623 766L958 1456H1238L750 536V0H496V536L7 1456H288L623 766Z" />
<glyph unicode="Z" horiz-adv-x="1233" d="M386 202H1164V0H80V164L833 1252H85V1456H1140V1296L386 202Z" />
<glyph unicode="[" horiz-adv-x="561" d="M540 1488H375V-135H540V-324H132V1678H540V1488Z" />
<glyph unicode="\" horiz-adv-x="856" d="M20 1456H260L868 -125H628L20 1456Z" />
<glyph unicode="]" horiz-adv-x="561" d="M12 1678H422V-324H12V-135H179V1488H12V1678Z" />
<glyph unicode="^" horiz-adv-x="875" d="M437 1190L259 729H53L352 1456H523L821 729H616L437 1190Z" />
<glyph unicode="_" horiz-adv-x="924" d="M920 -191H3V0H920V-191Z" />
<glyph unicode="`" horiz-adv-x="660" d="M521 1233H319L49 1536H326L521 1233Z" />
<glyph unicode="a" horiz-adv-x="1108" d="M771 0Q755 31 743 101Q627 -20 459 -20Q296 -20 193 73T90 303Q90 476 218 568T586 661H735V732Q735 816 688 866T545 917Q462 917 409 876T356 770H113Q113 859 172 936T332 1058T559 1102Q749 1102 862 1007T978 738V250Q978
104 1019 17V0H771ZM504 175Q576 175 639 210T735 304V508H604Q469 508 401 461T333 328Q333 258 379 217T504 175Z" />
<glyph unicode="b" horiz-adv-x="1153" d="M1074 530Q1074 278 962 129T652 -20Q462 -20 356 117L344 0H124V1536H367V978Q472 1102 650 1102Q848 1102 961 955T1074 544V530ZM831 551Q831 727 769 815T589 903Q431 903 367 765V319Q432 178 591 178Q705 178 767
263T831 520V551Z" />
<glyph unicode="c" horiz-adv-x="1072" d="M569 174Q660 174 720 227T784 358H1013Q1009 257 950 170T790 31T572 -20Q345 -20 212 127T79 533V558Q79 805 211 953T571 1102Q764 1102 885 990T1013 694H784Q780 787 721 847T569 907Q451 907 387 822T322 562V523Q322
347 385 261T569 174Z" />
<glyph unicode="d" horiz-adv-x="1156" d="M79 549Q79 799 195 950T506 1102Q678 1102 784 982V1536H1027V0H807L795 112Q686 -20 504 -20Q314 -20 197 133T79 549ZM322 528Q322 363 385 271T566 178Q715 178 784 311V773Q717 903 568 903Q450 903 386 810T322 528Z" />
<glyph unicode="e" horiz-adv-x="1099" d="M601 -20Q370 -20 227 125T83 513V543Q83 705 145 832T321 1031T573 1102Q794 1102 914 961T1035 562V464H328Q339 330 417 252T615 174Q782 174 887 309L1018 184Q953 87 845 34T601 -20ZM572 907Q472 907 411 837T332
642H795V660Q787 782 730 844T572 907Z" />
<glyph unicode="f" horiz-adv-x="726" d="M210 0V902H45V1082H210V1181Q210 1361 310 1459T590 1557Q654 1557 726 1539L720 1349Q680 1357 627 1357Q453 1357 453 1178V1082H673V902H453V0H210Z" />
<glyph unicode="g" horiz-adv-x="1161" d="M82 549Q82 801 200 951T515 1102Q700 1102 806 973L817 1082H1036V33Q1036 -180 904 -303T546 -426Q427 -426 314 -377T141 -247L256 -101Q368 -234 532 -234Q653 -234 723 -169T793 24V97Q688 -20 513 -20Q323 -20
203 131T82 549ZM324 528Q324 365 390 272T575 178Q722 178 793 304V780Q724 903 577 903Q457 903 391 808T324 528Z" />
<glyph unicode="h" horiz-adv-x="1137" d="M364 964Q483 1102 665 1102Q1011 1102 1016 707V0H773V698Q773 810 725 856T582 903Q436 903 364 773V0H121V1536H364V964Z" />
<glyph unicode="i" horiz-adv-x="523" d="M383 0H140V1082H383V0ZM125 1363Q125 1419 160 1456T262 1493T364 1456T400 1363Q400 1308 364 1272T262 1235T161 1271T125 1363Z" />
<glyph unicode="j" horiz-adv-x="513" d="M378 1082V-96Q378 -262 296 -349T54 -437Q-13 -437 -75 -420V-228Q-37 -237 11 -237Q132 -237 135 -105V1082H378ZM114 1363Q114 1419 149 1456T251 1493T353 1456T389 1363Q389 1308 353 1272T251 1235T150 1271T114 1363Z" />
<glyph unicode="k" horiz-adv-x="1069" d="M476 464L368 353V0H125V1536H368V650L444 745L743 1082H1035L633 631L1078 0H797L476 464Z" />
<glyph unicode="l" horiz-adv-x="523" d="M383 0H140V1536H383V0Z" />
<glyph unicode="m" horiz-adv-x="1782" d="M353 1082L360 969Q474 1102 672 1102Q889 1102 969 936Q1087 1102 1301 1102Q1480 1102 1567 1003T1657 711V0H1414V704Q1414 807 1369 855T1220 903Q1137 903 1085 859T1011 742L1012 0H769V712Q764 903 574 903Q428
903 367 784V0H124V1082H353Z" />
<glyph unicode="n" horiz-adv-x="1139" d="M350 1082L357 957Q477 1102 672 1102Q1010 1102 1016 715V0H773V701Q773 804 729 853T583 903Q436 903 364 770V0H121V1082H350Z" />
<glyph unicode="o" horiz-adv-x="1166" d="M79 551Q79 710 142 837T319 1033T581 1102Q800 1102 936 961T1084 587L1085 530Q1085 370 1024 244T848 49T583 -20Q354 -20 217 132T79 539V551ZM322 530Q322 363 391 269T583 174T774 270T843 551Q843 715 773 811T581
907Q462 907 392 813T322 530Z" />
<glyph unicode="p" horiz-adv-x="1153" d="M1072 530Q1072 279 958 130T652 -20Q474 -20 367 97V-416H124V1082H348L358 972Q465 1102 649 1102Q847 1102 959 955T1072 545V530ZM830 551Q830 713 766 808T581 903Q432 903 367 780V300Q433 174 583 174Q699 174
764 267T830 551Z" />
<glyph unicode="q" horiz-adv-x="1163" d="M79 550Q79 804 195 953T509 1102Q690 1102 796 975L810 1082H1026V-416H783V92Q677 -20 507 -20Q313 -20 196 131T79 550ZM322 529Q322 363 387 269T569 174Q713 174 783 297V789Q713 907 571 907Q455 907 389 814T322 529Z" />
<glyph unicode="r" horiz-adv-x="720" d="M691 860Q643 868 592 868Q425 868 367 740V0H124V1082H356L362 961Q450 1102 606 1102Q658 1102 692 1088L691 860Z" />
<glyph unicode="s" horiz-adv-x="1057" d="M731 294Q731 359 678 393T500 453T293 519Q111 607 111 774Q111 914 229 1008T529 1102Q723 1102 842 1006T962 757H719Q719 827 667 873T529 920Q449 920 399 883T348 784Q348 728 395 697T585 635T809 560T930 455T970
307Q970 161 849 71T532 -20Q399 -20 295 28T133 160T75 341H311Q316 255 376 209T535 162Q631 162 681 198T731 294Z" />
<glyph unicode="t" horiz-adv-x="681" d="M429 1345V1082H620V902H429V298Q429 236 453 209T541 181Q583 181 626 191V3Q543 -20 466 -20Q186 -20 186 289V902H8V1082H186V1345H429Z" />
<glyph unicode="u" horiz-adv-x="1138" d="M780 106Q673 -20 476 -20Q300 -20 210 83T119 381V1082H362V384Q362 178 533 178Q710 178 772 305V1082H1015V0H786L780 106Z" />
<glyph unicode="v" horiz-adv-x="1013" d="M506 308L735 1082H986L611 0H400L22 1082H274L506 308Z" />
<glyph unicode="w" horiz-adv-x="1522" d="M1075 335L1247 1082H1484L1189 0H989L757 743L529 0H329L33 1082H270L445 343L667 1082H850L1075 335Z" />
<glyph unicode="x" horiz-adv-x="1030" d="M513 726L719 1082H989L658 549L1000 0H732L516 370L301 0H31L373 549L43 1082H311L513 726Z" />
<glyph unicode="y" horiz-adv-x="997" d="M503 348L723 1082H982L552 -164Q453 -437 216 -437Q163 -437 99 -419V-231L145 -234Q237 -234 283 -201T357 -88L392 5L12 1082H274L503 348Z" />
<glyph unicode="z" horiz-adv-x="1030" d="M384 194H960V0H82V159L631 886H92V1082H939V928L384 194Z" />
<glyph unicode="{" horiz-adv-x="687" d="M609 -360Q256 -261 249 91V304Q249 529 56 529V707Q249 707 249 933V1145Q252 1325 342 1436T609 1597L657 1457Q484 1401 478 1151V935Q478 710 305 619Q478 527 478 300V87Q484 -163 657 -219L609 -360Z" />
<glyph unicode="|" horiz-adv-x="514" d="M341 -270H174V1456H341V-270Z" />
<glyph unicode="}" horiz-adv-x="687" d="M27 -219Q203 -162 207 93V301Q207 532 389 618Q207 704 207 938V1145Q203 1400 27 1457L75 1597Q257 1546 346 1432T435 1132V932Q435 707 629 707V529Q435 529 435 304V107Q435 -80 346 -194T75 -360L27 -219Z" />
<glyph unicode="~" horiz-adv-x="1361" d="M1244 786Q1244 610 1149 499T912 387Q838 387 776 415T636 511T526 596T454 613Q387 613 349 561T310 425H117Q117 596 208 705T447 815Q521 815 587 786T726 690T832 607T905 590Q972 590 1014 646T1056 786H1244Z" />
<glyph unicode="&#xa0;" horiz-adv-x="510" />
<glyph unicode="&#xa1;" horiz-adv-x="542" d="M170 662H379L403 -364H146L170 662ZM409 971Q409 915 373 878T272 840Q206 840 170 877T134 971Q134 1026 170 1063T272 1101Q337 1101 373 1064T409 971Z" />
<glyph unicode="&#xa2;" horiz-adv-x="1149" d="M591 174Q680 174 740 226T806 358H1034Q1030 222 932 120T687 -11V-245H487V-11Q304 23 202 166T100 530V558Q100 771 202 915T487 1093V1318H687V1094Q845 1066 937 958T1034 694H806Q799 790 740 848T590 907Q360
907 344 595L343 523Q343 347 406 261T591 174Z" />
<glyph unicode="&#xa3;" horiz-adv-x="1205" d="M509 598L516 422Q516 287 452 202H1148L1147 0H98V202H180Q219 211 240 266T262 413L255 598H94V797H249L241 1039Q241 1241 366 1358T694 1475T1013 1366T1129 1073H884Q884 1168 832 1220T685 1273Q596 1273
545 1208T493 1039L502 797H813V598H509Z" />
<glyph unicode="&#xa4;" horiz-adv-x="1437" d="M1085 107Q926 -20 723 -20Q521 -20 363 106L234 -26L93 118L228 255Q128 411 128 608Q128 808 237 973L93 1120L234 1264L376 1119Q531 1234 723 1234Q917 1234 1072 1117L1217 1265L1359 1120L1211 969Q1318 810
1318 608Q1318 415 1220 259L1359 118L1217 -27L1085 107ZM313 608Q313 488 368 385T518 224T723 165T928 224T1077 386T1132 608T1078 829T929 989T723 1048T517 990T368 829T313 608Z" />
<glyph unicode="&#xa5;" horiz-adv-x="1088" d="M545 847L807 1456H1076L735 742H969V590H666V452H969V301H666V0H414V301H106V452H414V590H106V742H354L11 1456H284L545 847Z" />
<glyph unicode="&#xa6;" horiz-adv-x="508" d="M136 -270V525H365V-270H136ZM365 698H136V1456H365V698Z" />
<glyph unicode="&#xa7;" horiz-adv-x="1272" d="M1164 455Q1164 271 993 182Q1128 82 1128 -103Q1128 -276 993 -375T624 -474Q378 -474 234 -366T90 -50L332 -49Q332 -159 410 -219T624 -279Q745 -279 815 -232T886 -105Q886 -28 819 17T565 118Q377 169 282
224T141 356T94 542Q94 726 263 816Q198 866 164 934T130 1102Q130 1272 267 1374T635 1476Q875 1476 1009 1364T1143 1047H900Q900 1153 828 1217T635 1281Q512 1281 443 1234T373 1104Q373 1020 433 977T686 881T977 773T1119 640T1164 455ZM601 673Q520 694
444 722Q336 682 336 558Q336 477 385 434T584 344L763 291L809 275Q924 322 924 439Q924 520 856 568T601 673Z" />
<glyph unicode="&#xa8;" horiz-adv-x="901" d="M93 1366Q93 1416 126 1450T219 1484T312 1450T346 1366T312 1282T219 1248T127 1282T93 1366ZM550 1365Q550 1415 583 1449T676 1483T769 1449T803 1365T769 1281T676 1247T584 1281T550 1365Z" />
<glyph unicode="&#xa9;" horiz-adv-x="1604" d="M1118 596Q1118 444 1031 363T783 282T529 388T434 675V788Q434 962 529 1068T783 1175Q946 1175 1032 1093T1119 861H963Q963 957 917 998T783 1040Q691 1040 640 972T588 786V669Q588 551 640 484T783 417Q872
417 917 457T962 596H1118ZM1384 729Q1384 895 1309 1037T1097 1265T797 1351Q638 1351 502 1269T287 1043T209 729T286 415T500 188T797 104T1094 189T1308 418T1384 729ZM87 729Q87 931 180 1104T439 1376T797 1476T1154 1377T1412 1104T1506 729T1413 354T1155
81T797 -20Q604 -20 440 80T181 353T87 729Z" />
<glyph unicode="&#xaa;" horiz-adv-x="913" d="M608 705L591 773Q514 691 390 691Q272 691 207 752T141 919Q141 1029 225 1089T482 1150H584V1201Q584 1328 468 1328Q403 1328 367 1303T330 1229L157 1243Q157 1347 244 1411T468 1476Q605 1476 682 1404T759
1199V883Q759 786 785 705H608ZM433 835Q473 835 515 853T584 896V1033H478Q402 1032 359 1002T316 923Q316 835 433 835Z" />
<glyph unicode="&#xab;" horiz-adv-x="994" d="M551 537L798 138H631L343 528V547L631 937H798L551 537ZM654 537L901 138H734L446 528V547L734 937H901L654 537Z" />
<glyph unicode="&#xac;" horiz-adv-x="1133" d="M962 374H762V634H127V805H962V374Z" />
<glyph unicode="&#xad;" horiz-adv-x="672" d="M596 521H71V717H596V521Z" />
<glyph unicode="&#xae;" horiz-adv-x="1604" d="M87 729Q87 931 180 1104T439 1376T797 1476T1154 1377T1412 1104T1506 729T1413 354T1155 81T797 -20Q604 -20 440 80T181 353T87 729ZM1384 729Q1384 895 1309 1037T1097 1265T797 1351Q638 1351 502 1269T287
1043T209 729T286 415T500 188T797 104T1094 189T1308 418T1384 729ZM653 653V316H502V1166H783Q936 1166 1022 1099T1108 906Q1108 789 988 726Q1053 697 1079 642T1105 505T1108 389T1122 332V316H967Q954 350 954 510Q954 586 921 619T811 653H653ZM653 787H796Q865
787 911 818T958 903Q958 973 923 1002T794 1033H653V787Z" />
<glyph unicode="&#xaf;" horiz-adv-x="987" d="M842 1292H155V1450H842V1292Z" />
<glyph unicode="&#xb0;" horiz-adv-x="778" d="M391 1476Q497 1476 574 1397T651 1208T575 1021T391 943Q282 943 205 1020T127 1208T205 1397T391 1476ZM391 1084Q444 1084 478 1119T513 1208Q513 1260 479 1298T391 1336T302 1298T266 1208T302 1120T391 1084Z" />
<glyph unicode="&#xb1;" horiz-adv-x="1098" d="M668 899H1011V700H668V312H452V700H95V899H452V1276H668V899ZM974 1H125V197H974V1Z" />
<glyph unicode="&#xb2;" horiz-adv-x="758" d="M690 667H78V792L363 1053Q476 1156 476 1223Q476 1265 449 1291T370 1318Q312 1318 279 1285T246 1198H60Q60 1314 144 1390T364 1467Q507 1467 585 1403T663 1224Q663 1117 557 1015L459 928L319 815H690V667Z" />
<glyph unicode="&#xb3;" horiz-adv-x="758" d="M268 1133H349Q481 1133 481 1230Q481 1265 454 1291T365 1318Q317 1318 285 1299T252 1244H66Q66 1343 148 1405T361 1467Q504 1467 585 1407T667 1241Q667 1122 532 1071Q681 1030 681 888Q681 782 593 719T361
656Q226 656 141 719T55 896H241Q241 858 275 832T370 805Q433 805 463 832T494 902Q494 1003 360 1004H268V1133Z" />
<glyph unicode="&#xb4;" horiz-adv-x="667" d="M307 1536H584L307 1233H112L307 1536Z" />
<glyph unicode="&#xb5;" horiz-adv-x="1211" d="M388 1082V446Q390 305 434 240T585 175Q753 175 812 296V1082H1055V0H832L825 86Q733 -21 586 -21Q465 -21 388 34V-416H146V1082H388Z" />
<glyph unicode="&#xb6;" horiz-adv-x="1005" d="M644 0V520H564Q334 520 202 647T69 988Q69 1201 202 1328T565 1456H854V0H644Z" />
<glyph unicode="&#xb7;" horiz-adv-x="578" d="M142 714Q142 772 179 811T283 850T387 811T425 714Q425 655 386 618T283 581Q218 581 180 618T142 714Z" />
<glyph unicode="&#xb8;" horiz-adv-x="528" d="M318 3L307 -51Q457 -78 457 -224Q457 -329 371 -388T130 -447L123 -310Q189 -310 224 -287T260 -221Q260 -176 225 -159T109 -136L141 3H318Z" />
<glyph unicode="&#xb9;" horiz-adv-x="758" d="M514 667H329V1237L128 1189V1335L495 1454H514V667Z" />
<glyph unicode="&#xba;" horiz-adv-x="935" d="M119 1121Q119 1281 214 1378T465 1476T716 1379T812 1116V1044Q812 885 718 788T467 690Q309 690 214 788T119 1049V1121ZM294 1044Q294 946 340 891T467 836Q545 836 590 890T637 1041V1121Q637 1218 591 1273T465
1328Q387 1328 341 1274T294 1117V1044Z" />
<glyph unicode="&#xbb;" horiz-adv-x="994" d="M260 937L548 547V528L260 138H93L340 537L93 937H260ZM633 937L921 547V528L633 138H466L713 537L466 937H633Z" />
<glyph unicode="&#xbc;" horiz-adv-x="1488" d="M475 664H290V1234L89 1186V1332L456 1451H475V664ZM453 117L328 189L1039 1327L1164 1255L453 117ZM1316 314H1411V163H1316V0H1129V163H771L762 284L1127 789H1316V314ZM943 314H1129V556L1115 534L943 314Z" />
<glyph unicode="&#xbd;" horiz-adv-x="1579" d="M410 117L285 189L996 1327L1121 1255L410 117ZM466 667H281V1237L80 1189V1335L447 1454H466V667ZM1484 0H872V125L1157 386Q1270 489 1270 556Q1270 598 1243 624T1164 651Q1106 651 1073 618T1040 531H854Q854
647 938 723T1158 800Q1301 800 1379 736T1457 557Q1457 450 1351 348L1253 261L1113 148H1484V0Z" />
<glyph unicode="&#xbe;" horiz-adv-x="1623" d="M594 117L469 189L1180 1327L1305 1255L594 117ZM1437 314H1532V163H1437V0H1250V163H892L883 284L1248 789H1437V314ZM1064 314H1250V556L1236 534L1064 314ZM316 1133H397Q529 1133 529 1230Q529 1265 502 1291T413
1318Q365 1318 333 1299T300 1244H114Q114 1343 196 1405T409 1467Q552 1467 633 1407T715 1241Q715 1122 580 1071Q729 1030 729 888Q729 782 641 719T409 656Q274 656 189 719T103 896H289Q289 858 323 832T418 805Q481 805 511 832T542 902Q542 1003 408 1004H316V1133Z"
/>
<glyph unicode="&#xbf;" horiz-adv-x="996" d="M630 661Q628 537 602 465T502 313L399 207Q309 110 309 4Q309 -90 358 -136T496 -183Q584 -183 637 -133T690 0H933Q931 -177 812 -281T498 -385Q292 -385 179 -285T66 0Q66 165 221 328L313 421Q391 493 401 608L403
661H630ZM650 972Q650 916 615 879T513 841T411 878T375 972Q375 1027 411 1064T513 1102T614 1065T650 972Z" />
<glyph unicode="&#xc0;" horiz-adv-x="1363" d="M963 339H399L281 0H18L568 1456H795L1346 0H1082L963 339ZM470 543H892L681 1147L470 543ZM812 1543H610L340 1846H617L812 1543Z" />
<glyph unicode="&#xc1;" horiz-adv-x="1363" d="M963 339H399L281 0H18L568 1456H795L1346 0H1082L963 339ZM470 543H892L681 1147L470 543ZM757 1846H1034L757 1543H562L757 1846Z" />
<glyph unicode="&#xc2;" horiz-adv-x="1363" d="M963 339H399L281 0H18L568 1456H795L1346 0H1082L963 339ZM470 543H892L681 1147L470 543ZM1030 1569V1558H835L685 1714L536 1558H343V1571L614 1847H757L1030 1569Z" />
<glyph unicode="&#xc3;" horiz-adv-x="1363" d="M963 339H399L281 0H18L568 1456H795L1346 0H1082L963 339ZM470 543H892L681 1147L470 543ZM1052 1824Q1052 1714 989 1641T829 1568Q790 1568 762 1576T681 1615T607 1651T559 1657Q521 1657 495 1629T468 1554L319
1562Q319 1672 382 1747T541 1822Q598 1822 678 1777T811 1732Q849 1732 876 1760T903 1836L1052 1824Z" />
<glyph unicode="&#xc4;" horiz-adv-x="1363" d="M963 339H399L281 0H18L568 1456H795L1346 0H1082L963 339ZM470 543H892L681 1147L470 543ZM331 1676Q331 1726 364 1760T457 1794T550 1760T584 1676T550 1592T457 1558T365 1592T331 1676ZM788 1675Q788 1725
821 1759T914 1793T1007 1759T1041 1675T1007 1591T914 1557T822 1591T788 1675Z" />
<glyph unicode="&#xc5;" horiz-adv-x="1363" d="M963 339H399L281 0H18L568 1456H795L1346 0H1082L963 339ZM470 543H892L681 1147L470 543ZM686 1940Q779 1940 843 1879T907 1732T845 1587T686 1527Q589 1527 527 1587T464 1732T527 1878T686 1940ZM574 1732Q574
1685 607 1653T686 1620Q733 1620 765 1652T798 1732Q798 1778 767 1811T686 1845T606 1812T574 1732Z" />
<glyph unicode="&#xc6;" horiz-adv-x="1925" d="M1879 0H981L966 340H464L280 0H-10L825 1456H1817V1259H1171L1188 851H1736V654H1196L1216 196H1879V0ZM580 555H957L930 1203L580 555Z" />
<glyph unicode="&#xc7;" horiz-adv-x="1337" d="M1259 474Q1237 241 1087 111T688 -20Q514 -20 382 62T177 297T102 650V786Q102 992 175 1149T384 1391T700 1476Q941 1476 1088 1345T1259 975H1007Q989 1132 916 1201T700 1271Q535 1271 447 1151T356 797V668Q356
432 440 308T688 184Q837 184 912 251T1007 474H1259ZM775 -2L764 -56Q914 -83 914 -229Q914 -334 828 -393T587 -452L580 -315Q646 -315 681 -292T717 -226Q717 -181 682 -164T566 -141L598 -2H775Z" />
<glyph unicode="&#xc8;" horiz-adv-x="1158" d="M999 650H401V202H1100V0H148V1456H1093V1252H401V850H999V650ZM753 1550H551L281 1853H558L753 1550Z" />
<glyph unicode="&#xc9;" horiz-adv-x="1158" d="M999 650H401V202H1100V0H148V1456H1093V1252H401V850H999V650ZM698 1853H975L698 1550H503L698 1853Z" />
<glyph unicode="&#xca;" horiz-adv-x="1158" d="M999 650H401V202H1100V0H148V1456H1093V1252H401V850H999V650ZM971 1576V1565H776L626 1721L477 1565H284V1578L555 1854H698L971 1576Z" />
<glyph unicode="&#xcb;" horiz-adv-x="1158" d="M999 650H401V202H1100V0H148V1456H1093V1252H401V850H999V650ZM272 1683Q272 1733 305 1767T398 1801T491 1767T525 1683T491 1599T398 1565T306 1599T272 1683ZM729 1682Q729 1732 762 1766T855 1800T948 1766T982
1682T948 1598T855 1564T763 1598T729 1682Z" />
<glyph unicode="&#xcc;" horiz-adv-x="578" d="M415 0H163V1456H415V0ZM416 1550H214L-56 1853H221L416 1550Z" />
<glyph unicode="&#xcd;" horiz-adv-x="578" d="M415 0H163V1456H415V0ZM360 1853H637L360 1550H165L360 1853Z" />
<glyph unicode="&#xce;" horiz-adv-x="578" d="M415 0H163V1456H415V0ZM634 1576V1565H439L289 1721L140 1565H-53V1578L218 1854H361L634 1576Z" />
<glyph unicode="&#xcf;" horiz-adv-x="578" d="M415 0H163V1456H415V0ZM-65 1683Q-65 1733 -32 1767T61 1801T154 1767T188 1683T154 1599T61 1565T-31 1599T-65 1683ZM392 1682Q392 1732 425 1766T518 1800T611 1766T645 1682T611 1598T518 1564T426 1598T392 1682Z" />
<glyph unicode="&#xd0;" horiz-adv-x="1368" d="M178 0V652H-9V822H178V1456H608Q801 1456 950 1370T1182 1126T1264 764V691Q1264 484 1182 327T947 85T597 0H178ZM660 652H431V202H594Q797 202 903 328T1010 695V765Q1010 1002 907 1127T608 1252H431V822H660V652Z" />
<glyph unicode="&#xd1;" horiz-adv-x="1454" d="M1303 0H1050L401 1033V0H148V1456H401L1052 419V1456H1303V0ZM1093 1824Q1093 1714 1030 1641T870 1568Q831 1568 803 1576T722 1615T648 1651T600 1657Q562 1657 536 1629T509 1554L360 1562Q360 1672 423 1747T582
1822Q639 1822 719 1777T852 1732Q890 1732 917 1760T944 1836L1093 1824Z" />
<glyph unicode="&#xd2;" horiz-adv-x="1414" d="M1310 690Q1310 476 1236 315T1025 67T708 -20Q531 -20 393 66T179 313T102 682V764Q102 977 177 1140T390 1389T706 1476T1021 1391T1234 1145T1310 771V690ZM1057 766Q1057 1008 966 1137T706 1266Q542 1266 450
1138T355 774V690Q355 450 448 319T708 188Q876 188 966 316T1057 690V766ZM835 1543H633L363 1846H640L835 1543Z" />
<glyph unicode="&#xd3;" horiz-adv-x="1414" d="M1310 690Q1310 476 1236 315T1025 67T708 -20Q531 -20 393 66T179 313T102 682V764Q102 977 177 1140T390 1389T706 1476T1021 1391T1234 1145T1310 771V690ZM1057 766Q1057 1008 966 1137T706 1266Q542 1266 450
1138T355 774V690Q355 450 448 319T708 188Q876 188 966 316T1057 690V766ZM780 1846H1057L780 1543H585L780 1846Z" />
<glyph unicode="&#xd4;" horiz-adv-x="1414" d="M1310 690Q1310 476 1236 315T1025 67T708 -20Q531 -20 393 66T179 313T102 682V764Q102 977 177 1140T390 1389T706 1476T1021 1391T1234 1145T1310 771V690ZM1057 766Q1057 1008 966 1137T706 1266Q542 1266 450
1138T355 774V690Q355 450 448 319T708 188Q876 188 966 316T1057 690V766ZM1053 1569V1558H858L708 1714L559 1558H366V1571L637 1847H780L1053 1569Z" />
<glyph unicode="&#xd5;" horiz-adv-x="1414" d="M1310 690Q1310 476 1236 315T1025 67T708 -20Q531 -20 393 66T179 313T102 682V764Q102 977 177 1140T390 1389T706 1476T1021 1391T1234 1145T1310 771V690ZM1057 766Q1057 1008 966 1137T706 1266Q542 1266 450
1138T355 774V690Q355 450 448 319T708 188Q876 188 966 316T1057 690V766ZM1075 1824Q1075 1714 1012 1641T852 1568Q813 1568 785 1576T704 1615T630 1651T582 1657Q544 1657 518 1629T491 1554L342 1562Q342 1672 405 1747T564 1822Q621 1822 701 1777T834 1732Q872
1732 899 1760T926 1836L1075 1824Z" />
<glyph unicode="&#xd6;" horiz-adv-x="1414" d="M1310 690Q1310 476 1236 315T1025 67T708 -20Q531 -20 393 66T179 313T102 682V764Q102 977 177 1140T390 1389T706 1476T1021 1391T1234 1145T1310 771V690ZM1057 766Q1057 1008 966 1137T706 1266Q542 1266 450
1138T355 774V690Q355 450 448 319T708 188Q876 188 966 316T1057 690V766ZM354 1676Q354 1726 387 1760T480 1794T573 1760T607 1676T573 1592T480 1558T388 1592T354 1676ZM811 1675Q811 1725 844 1759T937 1793T1030 1759T1064 1675T1030 1591T937 1557T845
1591T811 1675Z" />
<glyph unicode="&#xd7;" horiz-adv-x="1092" d="M77 364L393 686L77 1008L225 1158L540 836L856 1158L1004 1008L688 686L1004 364L856 214L540 535L225 214L77 364Z" />
<glyph unicode="&#xd8;" horiz-adv-x="1412" d="M1314 690Q1314 476 1240 315T1029 67T711 -20Q547 -20 415 55L324 -95H155L300 143Q105 338 105 697V764Q105 977 180 1139T393 1388T709 1476Q906 1476 1049 1375L1136 1518H1303L1156 1275Q1313 1082 1314 765V690ZM358
690Q358 483 429 355L931 1181Q844 1266 709 1266Q545 1266 453 1138T358 774V690ZM1061 766Q1061 932 1017 1046L528 242Q606 188 711 188Q880 188 970 316T1061 690V766Z" />
<glyph unicode="&#xd9;" horiz-adv-x="1335" d="M1213 1456V483Q1213 251 1065 116T669 -20Q419 -20 272 113T125 484V1456H377V482Q377 336 451 259T669 182Q961 182 961 490V1456H1213ZM794 1543H592L322 1846H599L794 1543Z" />
<glyph unicode="&#xda;" horiz-adv-x="1335" d="M1213 1456V483Q1213 251 1065 116T669 -20Q419 -20 272 113T125 484V1456H377V482Q377 336 451 259T669 182Q961 182 961 490V1456H1213ZM739 1846H1016L739 1543H544L739 1846Z" />
<glyph unicode="&#xdb;" horiz-adv-x="1335" d="M1213 1456V483Q1213 251 1065 116T669 -20Q419 -20 272 113T125 484V1456H377V482Q377 336 451 259T669 182Q961 182 961 490V1456H1213ZM1012 1569V1558H817L667 1714L518 1558H325V1571L596 1847H739L1012 1569Z" />
<glyph unicode="&#xdc;" horiz-adv-x="1335" d="M1213 1456V483Q1213 251 1065 116T669 -20Q419 -20 272 113T125 484V1456H377V482Q377 336 451 259T669 182Q961 182 961 490V1456H1213ZM313 1676Q313 1726 346 1760T439 1794T532 1760T566 1676T532 1592T439
1558T347 1592T313 1676ZM770 1675Q770 1725 803 1759T896 1793T989 1759T1023 1675T989 1591T896 1557T804 1591T770 1675Z" />
<glyph unicode="&#xdd;" horiz-adv-x="1248" d="M623 766L958 1456H1238L750 536V0H496V536L7 1456H288L623 766ZM698 1846H975L698 1543H503L698 1846Z" />
<glyph unicode="&#xde;" horiz-adv-x="1226" d="M391 1456V1176H632Q876 1176 1013 1057T1150 738Q1150 539 1013 420T633 300H391V0H148V1456H391ZM391 981V495H637Q762 495 834 560T907 736T837 913T645 981H391Z" />
<glyph unicode="&#xdf;" horiz-adv-x="1255" d="M378 0H136V1105Q136 1319 250 1438T571 1557Q758 1557 865 1464T973 1201Q973 1139 960 1090T912 985T866 896T855 824Q855 780 887 738T1009 622T1138 480T1179 336Q1179 165 1071 73T764 -20Q684 -20 599 -1T475
44L524 239Q569 211 632 193T750 174Q847 174 892 217T937 327Q937 376 902 421T780 535T653 671T612 819Q612 907 675 1007T738 1185Q738 1266 692 1314T566 1363Q382 1363 378 1116V0Z" />
<glyph unicode="&#xe0;" horiz-adv-x="1108" d="M771 0Q755 31 743 101Q627 -20 459 -20Q296 -20 193 73T90 303Q90 476 218 568T586 661H735V732Q735 816 688 866T545 917Q462 917 409 876T356 770H113Q113 859 172 936T332 1058T559 1102Q749 1102 862 1007T978
738V250Q978 104 1019 17V0H771ZM504 175Q576 175 639 210T735 304V508H604Q469 508 401 461T333 328Q333 258 379 217T504 175ZM694 1233H492L222 1536H499L694 1233Z" />
<glyph unicode="&#xe1;" horiz-adv-x="1108" d="M771 0Q755 31 743 101Q627 -20 459 -20Q296 -20 193 73T90 303Q90 476 218 568T586 661H735V732Q735 816 688 866T545 917Q462 917 409 876T356 770H113Q113 859 172 936T332 1058T559 1102Q749 1102 862 1007T978
738V250Q978 104 1019 17V0H771ZM504 175Q576 175 639 210T735 304V508H604Q469 508 401 461T333 328Q333 258 379 217T504 175ZM639 1536H916L639 1233H444L639 1536Z" />
<glyph unicode="&#xe2;" horiz-adv-x="1108" d="M771 0Q755 31 743 101Q627 -20 459 -20Q296 -20 193 73T90 303Q90 476 218 568T586 661H735V732Q735 816 688 866T545 917Q462 917 409 876T356 770H113Q113 859 172 936T332 1058T559 1102Q749 1102 862 1007T978
738V250Q978 104 1019 17V0H771ZM504 175Q576 175 639 210T735 304V508H604Q469 508 401 461T333 328Q333 258 379 217T504 175ZM912 1259V1248H717L567 1404L418 1248H225V1261L496 1537H639L912 1259Z" />
<glyph unicode="&#xe3;" horiz-adv-x="1108" d="M771 0Q755 31 743 101Q627 -20 459 -20Q296 -20 193 73T90 303Q90 476 218 568T586 661H735V732Q735 816 688 866T545 917Q462 917 409 876T356 770H113Q113 859 172 936T332 1058T559 1102Q749 1102 862 1007T978
738V250Q978 104 1019 17V0H771ZM504 175Q576 175 639 210T735 304V508H604Q469 508 401 461T333 328Q333 258 379 217T504 175ZM934 1514Q934 1404 871 1331T711 1258Q672 1258 644 1266T563 1305T489 1341T441 1347Q403 1347 377 1319T350 1244L201 1252Q201
1362 264 1437T423 1512Q480 1512 560 1467T693 1422Q731 1422 758 1450T785 1526L934 1514Z" />
<glyph unicode="&#xe4;" horiz-adv-x="1108" d="M771 0Q755 31 743 101Q627 -20 459 -20Q296 -20 193 73T90 303Q90 476 218 568T586 661H735V732Q735 816 688 866T545 917Q462 917 409 876T356 770H113Q113 859 172 936T332 1058T559 1102Q749 1102 862 1007T978
738V250Q978 104 1019 17V0H771ZM504 175Q576 175 639 210T735 304V508H604Q469 508 401 461T333 328Q333 258 379 217T504 175ZM213 1366Q213 1416 246 1450T339 1484T432 1450T466 1366T432 1282T339 1248T247 1282T213 1366ZM670 1365Q670 1415 703 1449T796
1483T889 1449T923 1365T889 1281T796 1247T704 1281T670 1365Z" />
<glyph unicode="&#xe5;" horiz-adv-x="1108" d="M771 0Q755 31 743 101Q627 -20 459 -20Q296 -20 193 73T90 303Q90 476 218 568T586 661H735V732Q735 816 688 866T545 917Q462 917 409 876T356 770H113Q113 859 172 936T332 1058T559 1102Q749 1102 862 1007T978
738V250Q978 104 1019 17V0H771ZM504 175Q576 175 639 210T735 304V508H604Q469 508 401 461T333 328Q333 258 379 217T504 175ZM568 1630Q661 1630 725 1569T789 1422T727 1277T568 1217Q471 1217 409 1277T346 1422T409 1568T568 1630ZM456 1422Q456 1375 489
1343T568 1310Q615 1310 647 1342T680 1422Q680 1468 649 1501T568 1535T488 1502T456 1422Z" />
<glyph unicode="&#xe6;" horiz-adv-x="1729" d="M1254 -20Q1001 -20 861 141Q796 64 689 22T448 -20Q272 -20 172 68T72 312Q72 470 191 556T543 642H734V713Q734 804 687 855T551 907Q460 907 403 863T345 752L103 771Q103 917 229 1009T553 1102Q776 1102 887
969Q1018 1104 1218 1102Q1430 1102 1549 973T1668 608V471H973Q982 332 1058 253T1268 174Q1405 174 1512 232L1573 266L1646 100Q1576 44 1472 12T1254 -20ZM495 164Q553 164 621 193T734 266V475H538Q434 473 374 426T314 308Q314 243 360 204T495 164ZM1218
907Q1119 907 1056 838T976 642H1428V672Q1428 785 1374 846T1218 907Z" />
<glyph unicode="&#xe7;" horiz-adv-x="1072" d="M569 174Q660 174 720 227T784 358H1013Q1009 257 950 170T790 31T572 -20Q345 -20 212 127T79 533V558Q79 805 211 953T571 1102Q764 1102 885 990T1013 694H784Q780 787 721 847T569 907Q451 907 387 822T322
562V523Q322 347 385 261T569 174ZM635 -2L624 -56Q774 -83 774 -229Q774 -334 688 -393T447 -452L440 -315Q506 -315 541 -292T577 -226Q577 -181 542 -164T426 -141L458 -2H635Z" />
<glyph unicode="&#xe8;" horiz-adv-x="1099" d="M601 -20Q370 -20 227 125T83 513V543Q83 705 145 832T321 1031T573 1102Q794 1102 914 961T1035 562V464H328Q339 330 417 252T615 174Q782 174 887 309L1018 184Q953 87 845 34T601 -20ZM572 907Q472 907 411
837T332 642H795V660Q787 782 730 844T572 907ZM682 1233H480L210 1536H487L682 1233Z" />
<glyph unicode="&#xe9;" horiz-adv-x="1099" d="M601 -20Q370 -20 227 125T83 513V543Q83 705 145 832T321 1031T573 1102Q794 1102 914 961T1035 562V464H328Q339 330 417 252T615 174Q782 174 887 309L1018 184Q953 87 845 34T601 -20ZM572 907Q472 907 411
837T332 642H795V660Q787 782 730 844T572 907ZM627 1536H904L627 1233H432L627 1536Z" />
<glyph unicode="&#xea;" horiz-adv-x="1099" d="M601 -20Q370 -20 227 125T83 513V543Q83 705 145 832T321 1031T573 1102Q794 1102 914 961T1035 562V464H328Q339 330 417 252T615 174Q782 174 887 309L1018 184Q953 87 845 34T601 -20ZM572 907Q472 907 411
837T332 642H795V660Q787 782 730 844T572 907ZM900 1259V1248H705L555 1404L406 1248H213V1261L484 1537H627L900 1259Z" />
<glyph unicode="&#xeb;" horiz-adv-x="1099" d="M601 -20Q370 -20 227 125T83 513V543Q83 705 145 832T321 1031T573 1102Q794 1102 914 961T1035 562V464H328Q339 330 417 252T615 174Q782 174 887 309L1018 184Q953 87 845 34T601 -20ZM572 907Q472 907 411
837T332 642H795V660Q787 782 730 844T572 907ZM201 1366Q201 1416 234 1450T327 1484T420 1450T454 1366T420 1282T327 1248T235 1282T201 1366ZM658 1365Q658 1415 691 1449T784 1483T877 1449T911 1365T877 1281T784 1247T692 1281T658 1365Z" />
<glyph unicode="&#xec;" horiz-adv-x="538" d="M386 0H143V1082H386V0ZM652 1482H450L180 1785H457L652 1482Z" />
<glyph unicode="&#xed;" horiz-adv-x="538" d="M386 0H143V1082H386V0ZM340 1785H617L340 1482H145L340 1785Z" />
<glyph unicode="&#xee;" horiz-adv-x="538" d="M386 0H143V1082H386V0ZM614 1252V1241H419L269 1397L120 1241H-73V1254L198 1530H341L614 1252Z" />
<glyph unicode="&#xef;" horiz-adv-x="538" d="M386 0H143V1082H386V0ZM-85 1359Q-85 1409 -52 1443T41 1477T134 1443T168 1359T134 1275T41 1241T-51 1275T-85 1359ZM372 1358Q372 1408 405 1442T498 1476T591 1442T625 1358T591 1274T498 1240T406 1274T372 1358Z" />
<glyph unicode="&#xf0;" horiz-adv-x="1191" d="M834 1303Q1088 1038 1088 637V555Q1088 389 1025 258T848 53T593 -20Q455 -20 342 43T166 219T103 468Q103 617 159 732T319 910T554 973Q700 973 813 882Q764 1051 638 1173L434 1038L356 1147L528 1261Q402 1343
240 1385L315 1580Q553 1530 730 1395L910 1515L988 1406L834 1303ZM845 663L844 681Q812 729 751 757T611 785Q485 785 416 701T346 468Q346 342 416 258T597 174Q708 174 776 274T845 547V663Z" />
<glyph unicode="&#xf1;" horiz-adv-x="1139" d="M350 1082L357 957Q477 1102 672 1102Q1010 1102 1016 715V0H773V701Q773 804 729 853T583 903Q436 903 364 770V0H121V1082H350ZM940 1514Q940 1404 877 1331T717 1258Q678 1258 650 1266T569 1305T495 1341T447
1347Q409 1347 383 1319T356 1244L207 1252Q207 1362 270 1437T429 1512Q486 1512 566 1467T699 1422Q737 1422 764 1450T791 1526L940 1514Z" />
<glyph unicode="&#xf2;" horiz-adv-x="1166" d="M79 551Q79 710 142 837T319 1033T581 1102Q800 1102 936 961T1084 587L1085 530Q1085 370 1024 244T848 49T583 -20Q354 -20 217 132T79 539V551ZM322 530Q322 363 391 269T583 174T774 270T843 551Q843 715 773
811T581 907Q462 907 392 813T322 530ZM703 1233H501L231 1536H508L703 1233Z" />
<glyph unicode="&#xf3;" horiz-adv-x="1166" d="M79 551Q79 710 142 837T319 1033T581 1102Q800 1102 936 961T1084 587L1085 530Q1085 370 1024 244T848 49T583 -20Q354 -20 217 132T79 539V551ZM322 530Q322 363 391 269T583 174T774 270T843 551Q843 715 773
811T581 907Q462 907 392 813T322 530ZM648 1536H925L648 1233H453L648 1536Z" />
<glyph unicode="&#xf4;" horiz-adv-x="1166" d="M79 551Q79 710 142 837T319 1033T581 1102Q800 1102 936 961T1084 587L1085 530Q1085 370 1024 244T848 49T583 -20Q354 -20 217 132T79 539V551ZM322 530Q322 363 391 269T583 174T774 270T843 551Q843 715 773
811T581 907Q462 907 392 813T322 530ZM921 1259V1248H726L576 1404L427 1248H234V1261L505 1537H648L921 1259Z" />
<glyph unicode="&#xf5;" horiz-adv-x="1166" d="M79 551Q79 710 142 837T319 1033T581 1102Q800 1102 936 961T1084 587L1085 530Q1085 370 1024 244T848 49T583 -20Q354 -20 217 132T79 539V551ZM322 530Q322 363 391 269T583 174T774 270T843 551Q843 715 773
811T581 907Q462 907 392 813T322 530ZM943 1514Q943 1404 880 1331T720 1258Q681 1258 653 1266T572 1305T498 1341T450 1347Q412 1347 386 1319T359 1244L210 1252Q210 1362 273 1437T432 1512Q489 1512 569 1467T702 1422Q740 1422 767 1450T794 1526L943 1514Z"
/>
<glyph unicode="&#xf6;" horiz-adv-x="1166" d="M79 551Q79 710 142 837T319 1033T581 1102Q800 1102 936 961T1084 587L1085 530Q1085 370 1024 244T848 49T583 -20Q354 -20 217 132T79 539V551ZM322 530Q322 363 391 269T583 174T774 270T843 551Q843 715 773
811T581 907Q462 907 392 813T322 530ZM222 1366Q222 1416 255 1450T348 1484T441 1450T475 1366T441 1282T348 1248T256 1282T222 1366ZM679 1365Q679 1415 712 1449T805 1483T898 1449T932 1365T898 1281T805 1247T713 1281T679 1365Z" />
<glyph unicode="&#xf7;" horiz-adv-x="1169" d="M1079 582H67V794H1079V582ZM576 1228Q644 1228 681 1190T718 1095T681 1001T576 963Q509 963 472 1000T435 1095T472 1190T576 1228ZM435 278Q435 336 472 374T576 412Q644 412 681 374T718 278Q718 221 681 184T576
147Q509 147 472 184T435 278Z" />
<glyph unicode="&#xf8;" horiz-adv-x="1160" d="M79 551Q79 710 142 837T319 1033T581 1102Q687 1102 775 1068L846 1211H991L889 1003Q1085 850 1085 530Q1085 370 1024 244T848 49T583 -20Q490 -20 400 10L328 -137H183L285 70Q79 220 79 551ZM322 530Q322 374
386 276L685 885Q638 907 581 907Q462 907 392 813T322 530ZM843 551Q843 699 785 792L489 191Q532 174 583 174Q706 174 774 270T843 551Z" />
<glyph unicode="&#xf9;" horiz-adv-x="1138" d="M780 106Q673 -20 476 -20Q300 -20 210 83T119 381V1082H362V384Q362 178 533 178Q710 178 772 305V1082H1015V0H786L780 106ZM696 1233H494L224 1536H501L696 1233Z" />
<glyph unicode="&#xfa;" horiz-adv-x="1138" d="M780 106Q673 -20 476 -20Q300 -20 210 83T119 381V1082H362V384Q362 178 533 178Q710 178 772 305V1082H1015V0H786L780 106ZM641 1536H918L641 1233H446L641 1536Z" />
<glyph unicode="&#xfb;" horiz-adv-x="1138" d="M780 106Q673 -20 476 -20Q300 -20 210 83T119 381V1082H362V384Q362 178 533 178Q710 178 772 305V1082H1015V0H786L780 106ZM914 1259V1248H719L569 1404L420 1248H227V1261L498 1537H641L914 1259Z" />
<glyph unicode="&#xfc;" horiz-adv-x="1138" d="M780 106Q673 -20 476 -20Q300 -20 210 83T119 381V1082H362V384Q362 178 533 178Q710 178 772 305V1082H1015V0H786L780 106ZM215 1366Q215 1416 248 1450T341 1484T434 1450T468 1366T434 1282T341 1248T249 1282T215
1366ZM672 1365Q672 1415 705 1449T798 1483T891 1449T925 1365T891 1281T798 1247T706 1281T672 1365Z" />
<glyph unicode="&#xfd;" horiz-adv-x="997" d="M503 348L723 1082H982L552 -164Q453 -437 216 -437Q163 -437 99 -419V-231L145 -234Q237 -234 283 -201T357 -88L392 5L12 1082H274L503 348ZM585 1536H862L585 1233H390L585 1536Z" />
<glyph unicode="&#xfe;" horiz-adv-x="1175" d="M1079 530Q1079 283 966 132T658 -20Q480 -20 373 97V-416H130V1536H373V983Q479 1102 655 1102Q852 1102 965 955T1079 546V530ZM836 551Q836 717 771 810T587 903Q438 903 373 780V300Q439 174 589 174Q705 174
770 267T836 551Z" />
<glyph unicode="&#xff;" horiz-adv-x="997" d="M503 348L723 1082H982L552 -164Q453 -437 216 -437Q163 -437 99 -419V-231L145 -234Q237 -234 283 -201T357 -88L392 5L12 1082H274L503 348ZM159 1366Q159 1416 192 1450T285 1484T378 1450T412 1366T378 1282T285
1248T193 1282T159 1366ZM616 1365Q616 1415 649 1449T742 1483T835 1449T869 1365T835 1281T742 1247T650 1281T616 1365Z" />
<glyph unicode="&#x2013;" horiz-adv-x="1321" d="M1432 621H414V817H1432V621Z" />
<glyph unicode="&#x2014;" horiz-adv-x="1584" d="M1744 621H386V817H1744V621Z" />
<glyph unicode="&#x2018;" horiz-adv-x="448" d="M282 1562L406 1485Q315 1352 312 1208V1056H99V1194Q100 1290 151 1394T282 1562Z" />
<glyph unicode="&#x2019;" horiz-adv-x="444" d="M175 1024L51 1101Q141 1232 144 1378V1536H357V1398Q357 1295 305 1191T175 1024Z" />
<glyph unicode="&#x201a;" horiz-adv-x="462" d="M173 -298L50 -220Q135 -93 138 55V202H356V69Q355 -24 304 -128T173 -298Z" />
<glyph unicode="&#x201c;" horiz-adv-x="788" d="M291 1562L415 1485Q324 1352 321 1208V1056H108V1194Q109 1290 160 1394T291 1562ZM627 1562L751 1485Q660 1352 657 1208V1056H444V1194Q445 1290 496 1394T627 1562Z" />
<glyph unicode="&#x201d;" horiz-adv-x="795" d="M188 1024L64 1101Q154 1232 157 1378V1536H370V1398Q370 1295 318 1191T188 1024ZM522 1024L398 1101Q488 1232 491 1378V1536H704V1398Q704 1295 652 1191T522 1024Z" />
<glyph unicode="&#x201e;" horiz-adv-x="776" d="M177 -318L50 -240Q135 -103 138 54V255H356V69Q355 -39 300 -153Q251 -253 177 -318ZM499 -318L372 -240Q460 -98 464 52V255H682V73Q682 -26 631 -136T499 -318Z" />
<glyph unicode="&#x2022;" horiz-adv-x="715" d="M136 771Q136 866 196 926T357 987Q460 987 520 927T580 768V731Q580 637 521 578T358 518Q259 518 199 575T136 726V771Z" />
<glyph unicode="&#x2039;" horiz-adv-x="626" d="M316 537L563 138H396L108 528V547L396 937H563L316 537Z" />
<glyph unicode="&#x203a;" horiz-adv-x="617" d="M251 937L539 547V528L251 138H84L331 537L84 937H251Z" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,308 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<defs >
<font id="Roboto" horiz-adv-x="1158" ><font-face
font-family="Roboto"
units-per-em="2048"
panose-1="2 0 0 0 0 0 0 0 0 0"
ascent="1900"
descent="-500"
alphabetic="0" />
<glyph unicode=" " horiz-adv-x="507" />
<glyph unicode="!" horiz-adv-x="527" d="M347 411H180L167 1456H361L347 411ZM160 93Q160 138 187 168T269 199T351 169T379 93T351 19T269 -11T188 18T160 93Z" />
<glyph unicode="&quot;" horiz-adv-x="655" d="M277 1400L247 1042H136L137 1536H277V1400ZM547 1400L517 1042H406L407 1536H547V1400Z" />
<glyph unicode="#" horiz-adv-x="1261" d="M765 410H501L421 0H278L358 410H119V547H384L453 901H195V1040H480L562 1456H705L623 1040H887L969 1456H1113L1031 1040H1235V901H1004L935 547H1160V410H909L829 0H685L765 410ZM527 547H791L860 901H596L527 547Z" />
<glyph unicode="$" horiz-adv-x="1150" d="M856 375Q856 467 792 530T574 644Q361 709 264 813T166 1079Q166 1243 261 1348T524 1473V1692H673V1472Q841 1449 934 1331T1028 1008H844Q844 1149 777 1232T596 1315Q477 1315 414 1254T351 1082Q351 980 417 920T636
810T874 701T1000 562T1041 377Q1041 208 940 105T655 -17V-208H507V-17Q321 0 216 115T110 429H295Q295 290 368 215T575 140Q706 140 781 203T856 375Z" />
<glyph unicode="%" horiz-adv-x="1500" d="M105 1176Q105 1307 188 1392T403 1477Q536 1477 618 1392T701 1170V1099Q701 967 618 884T405 800Q275 800 190 883T105 1106V1176ZM243 1099Q243 1021 287 971T405 920Q476 920 519 969T563 1103V1176Q563 1254 520
1305T403 1356T286 1305T243 1172V1099ZM814 357Q814 488 897 572T1112 657T1327 573T1411 350V279Q1411 148 1328 64T1114 -21T899 62T814 285V357ZM952 279Q952 200 996 150T1114 99Q1186 99 1229 148T1272 283V357Q1272 436 1229 486T1112 536Q1041 536 997
487T952 353V279ZM447 110L342 176L1053 1314L1158 1248L447 110Z" />
<glyph unicode="&amp;" horiz-adv-x="1273" d="M101 391Q101 496 159 584T383 789Q286 907 253 979T220 1122Q220 1288 318 1382T584 1476Q734 1476 832 1389T930 1168Q930 1080 886 1006T730 849L623 770L947 383Q1015 513 1015 672H1182Q1182 417 1059 249L1267
0H1045L948 115Q874 49 775 15T572 -20Q359 -20 230 93T101 391ZM572 131Q719 131 841 243L486 668L453 644Q286 521 286 391Q286 273 362 202T572 131ZM405 1128Q405 1032 523 888L641 971Q709 1019 734 1062T759 1168Q759 1235 709 1279T583 1324Q501 1324 453
1269T405 1128Z" />
<glyph unicode="&apos;" horiz-adv-x="357" d="M253 1425L232 1057H103L104 1536H253V1425Z" />
<glyph unicode="(" horiz-adv-x="700" d="M133 591Q133 817 193 1025T374 1403T623 1643L661 1521Q515 1409 422 1179T319 664L318 579Q318 193 459 -91Q544 -261 661 -357L623 -470Q490 -396 369 -222Q133 118 133 591Z" />
<glyph unicode=")" horiz-adv-x="712" d="M567 581Q567 358 509 154T330 -224T77 -470L38 -357Q192 -239 285 9T381 561V593Q381 803 337 983T215 1307T38 1530L77 1643Q209 1570 328 1399T507 1022T567 581Z" />
<glyph unicode="*" horiz-adv-x="882" d="M330 983L28 1073L74 1224L376 1112L367 1456H520L510 1107L807 1217L853 1065L546 974L744 703L620 609L434 897L254 616L129 707L330 983Z" />
<glyph unicode="+" horiz-adv-x="1161" d="M670 781H1076V606H670V146H484V606H78V781H484V1206H670V781Z" />
<glyph unicode="," horiz-adv-x="402" d="M134 -290L29 -218Q123 -87 127 52V219H308V74Q308 -27 259 -128T134 -290Z" />
<glyph unicode="-" horiz-adv-x="565" d="M525 543H37V694H525V543Z" />
<glyph unicode="." horiz-adv-x="539" d="M144 97Q144 145 172 177T258 209T344 177T374 97Q374 51 345 20T258 -11T173 20T144 97Z" />
<glyph unicode="/" horiz-adv-x="844" d="M177 -125H18L626 1456H784L177 -125Z" />
<glyph unicode="0" horiz-adv-x="1150" d="M1034 621Q1034 296 923 138T576 -20Q343 -20 231 134T115 596V843Q115 1164 226 1320T574 1476Q809 1476 920 1326T1034 861V621ZM849 874Q849 1109 783 1216T574 1324Q432 1324 367 1217T300 888V592Q300 356 368 244T576
131Q713 131 779 237T849 571V874Z" />
<glyph unicode="1" horiz-adv-x="1150" d="M729 0H543V1233L170 1096V1264L700 1463H729V0Z" />
<glyph unicode="2" horiz-adv-x="1150" d="M1075 0H121V133L625 693Q737 820 779 899T822 1064Q822 1178 753 1251T569 1324Q431 1324 355 1246T278 1027H93Q93 1228 222 1352T569 1476Q772 1476 890 1370T1008 1086Q1008 871 734 574L344 151H1075V0Z" />
<glyph unicode="3" horiz-adv-x="1150" d="M390 818H529Q660 820 735 887T810 1068Q810 1324 555 1324Q435 1324 364 1256T292 1074H107Q107 1247 233 1361T555 1476Q761 1476 878 1367T995 1064Q995 969 934 880T766 747Q886 709 951 621T1017 406Q1017 210 889
95T556 -20T223 91T94 384H280Q280 269 355 200T556 131Q690 131 761 201T832 402Q832 529 754 597T529 667H390V818Z" />
<glyph unicode="4" horiz-adv-x="1150" d="M902 489H1104V338H902V0H716V338H53V447L705 1456H902V489ZM263 489H716V1203L694 1163L263 489Z" />
<glyph unicode="5" horiz-adv-x="1150" d="M206 730L280 1456H1026V1285H437L393 888Q500 951 636 951Q835 951 952 820T1069 464Q1069 239 948 110T608 -20Q415 -20 293 87T154 383H329Q346 258 418 195T608 131Q737 131 810 219T884 462Q884 608 805 696T593
785Q472 785 403 732L354 692L206 730Z" />
<glyph unicode="6" horiz-adv-x="1150" d="M847 1457V1300H813Q597 1296 469 1172T321 823Q436 955 635 955Q825 955 938 821T1052 475Q1052 250 930 115T601 -20Q392 -20 262 140T132 554V625Q132 1027 303 1239T814 1457H847ZM604 801Q509 801 429 744T318 601V533Q318
353 399 243T601 133Q726 133 797 225T869 466Q869 616 797 708T604 801Z" />
<glyph unicode="7" horiz-adv-x="1150" d="M1061 1352L458 0H264L865 1304H77V1456H1061V1352Z" />
<glyph unicode="8" horiz-adv-x="1150" d="M1004 1076Q1004 967 947 882T791 749Q905 700 971 606T1038 393Q1038 204 911 92T575 -20Q365 -20 239 92T112 393Q112 511 176 606T355 750Q258 798 202 883T146 1076Q146 1260 264 1368T575 1476Q767 1476 885 1368T1004
1076ZM853 397Q853 519 776 596T573 673T373 597T297 397T370 202T575 131Q705 131 779 202T853 397ZM575 1324Q466 1324 399 1257T331 1073Q331 962 397 894T575 825T752 893T819 1073T750 1254T575 1324Z" />
<glyph unicode="9" horiz-adv-x="1150" d="M830 640Q772 571 692 529T515 487Q389 487 296 549T151 723T100 972Q100 1118 155 1235T313 1414T551 1476Q767 1476 891 1315T1016 874V820Q1016 395 848 200T341 -1H305V155H344Q573 159 696 274T830 640ZM545 640Q638
640 716 697T831 838V912Q831 1094 752 1208T552 1322Q430 1322 356 1229T282 982Q282 833 353 737T545 640Z" />
<glyph unicode=":" horiz-adv-x="496" d="M390 97Q390 145 418 177T504 209T590 177T620 97Q620 51 591 20T504 -11T419 20T390 97ZM135 980Q135 1028 163 1060T249 1092T335 1060T365 980Q365 934 336 903T249 872T164 903T135 980Z" />
<glyph unicode=";" horiz-adv-x="433" d="M111 980Q111 1028 139 1060T225 1092T311 1060T341 980Q341 934 312 903T225 872T140 903T111 980ZM146 -290L41 -218Q135 -87 139 52V219H320V74Q320 -27 271 -128T146 -290Z" />
<glyph unicode="&lt;" horiz-adv-x="1041" d="M264 644L890 391V195L72 574V720L890 1098V902L264 644Z" />
<glyph unicode="=" horiz-adv-x="1124" d="M986 814H152V975H986V814ZM986 399H152V559H986V399Z" />
<glyph unicode="&gt;" horiz-adv-x="1070" d="M795 650L134 909V1099L988 721V575L134 196V388L795 650Z" />
<glyph unicode="?" horiz-adv-x="967" d="M357 410Q359 529 384 598T486 751L617 886Q701 981 701 1090Q701 1195 646 1254T486 1314Q384 1314 322 1260T260 1115H75Q77 1277 190 1376T486 1476Q675 1476 780 1375T886 1096Q886 921 724 751L615 643Q542 562 542
410H357ZM349 93Q349 138 376 168T458 199T540 169T568 93T540 19T458 -11T377 18T349 93Z" />
<glyph unicode="@" horiz-adv-x="1839" d="M1738 502Q1726 260 1618 120T1329 -20Q1142 -20 1089 148Q1035 63 966 22T822 -20Q680 -20 607 96T553 417Q568 582 628 711T784 915T985 989Q1066 989 1130 968T1274 883L1222 329Q1203 98 1350 98Q1463 98 1533 210T1609
502Q1628 891 1465 1095T967 1299Q766 1299 610 1200T364 912T263 478Q251 230 323 48T542 -231T899 -328Q989 -328 1079 -306T1230 -249L1267 -364Q1205 -403 1103 -428T895 -453Q645 -453 465 -341T196 -17T118 478Q130 753 241 972T542 1311T971 1431Q1220 1431
1398 1319T1663 996T1738 502ZM712 417Q698 275 738 199T867 123Q927 123 982 174T1074 320L1075 329L1121 832Q1065 861 1001 861Q884 861 808 742T712 417Z" />
<glyph unicode="A" horiz-adv-x="1336" d="M973 380H363L226 0H28L584 1456H752L1309 0H1112L973 380ZM421 538H916L668 1219L421 538Z" />
<glyph unicode="B" horiz-adv-x="1275" d="M169 0V1456H645Q882 1456 1001 1358T1121 1068Q1121 966 1063 888T905 766Q1023 733 1091 641T1160 420Q1160 224 1033 112T674 0H169ZM361 681V157H678Q812 157 889 226T967 418Q967 681 681 681H361ZM361 835H651Q777
835 852 898T928 1069Q928 1189 858 1243T645 1298H361V835Z" />
<glyph unicode="C" horiz-adv-x="1333" d="M1240 462Q1213 231 1070 106T688 -20Q430 -20 275 165T119 660V800Q119 1003 191 1157T397 1393T705 1476Q937 1476 1077 1347T1240 988H1047Q1022 1162 939 1240T705 1318Q521 1318 417 1182T312 795V654Q312 417 411
277T688 137Q848 137 933 209T1047 462H1240Z" />
<glyph unicode="D" horiz-adv-x="1343" d="M169 0V1456H580Q770 1456 916 1372T1141 1133T1222 777V684Q1222 478 1143 323T916 85T572 0H169ZM361 1298V157H563Q785 157 908 295T1032 688V773Q1032 1021 916 1158T585 1298H361Z" />
<glyph unicode="E" horiz-adv-x="1164" d="M992 673H361V157H1094V0H169V1456H1084V1298H361V830H992V673Z" />
<glyph unicode="F" horiz-adv-x="1132" d="M972 643H361V0H169V1456H1071V1298H361V800H972V643Z" />
<glyph unicode="G" horiz-adv-x="1395" d="M1244 191Q1170 85 1038 33T729 -20Q551 -20 413 63T200 301T122 658V785Q122 1114 275 1295T707 1476Q935 1476 1074 1360T1244 1029H1052Q998 1318 708 1318Q515 1318 416 1183T315 790V671Q315 426 427 282T730 137Q838
137 919 161T1053 242V569H716V725H1244V191Z" />
<glyph unicode="H" horiz-adv-x="1460" d="M1288 0H1095V673H361V0H169V1456H361V830H1095V1456H1288V0Z" />
<glyph unicode="I" horiz-adv-x="557" d="M375 0H183V1456H375V0Z" />
<glyph unicode="J" horiz-adv-x="1130" d="M779 1456H972V425Q972 216 847 98T512 -20Q295 -20 174 91T53 402H245Q245 277 313 207T512 137Q631 137 704 212T779 422V1456Z" />
<glyph unicode="K" horiz-adv-x="1284" d="M539 677L361 492V0H169V1456H361V736L1008 1456H1240L667 813L1285 0H1055L539 677Z" />
<glyph unicode="L" horiz-adv-x="1102" d="M362 157H1052V0H169V1456H362V157Z" />
<glyph unicode="M" horiz-adv-x="1788" d="M417 1456L893 268L1369 1456H1618V0H1426V567L1444 1179L966 0H819L342 1176L361 567V0H169V1456H417Z" />
<glyph unicode="N" horiz-adv-x="1460" d="M1288 0H1095L362 1122V0H169V1456H362L1097 329V1456H1288V0Z" />
<glyph unicode="O" horiz-adv-x="1408" d="M1289 681Q1289 467 1217 308T1013 64T705 -20Q533 -20 400 64T194 305T118 668V773Q118 983 191 1144T397 1390T703 1476Q878 1476 1011 1392T1217 1147T1289 773V681ZM1098 775Q1098 1034 994 1172T703 1311Q521 1311
417 1173T309 788V681Q309 430 414 287T705 143Q891 143 993 278T1098 667V775Z" />
<glyph unicode="P" horiz-adv-x="1292" d="M361 570V0H169V1456H706Q945 1456 1080 1334T1216 1011Q1216 799 1084 685T704 570H361ZM361 727H706Q860 727 942 799T1024 1009Q1024 1139 942 1217T717 1298H361V727Z" />
<glyph unicode="Q" horiz-adv-x="1408" d="M1281 681Q1281 470 1214 318T1026 79L1286 -125L1155 -246L848 -2Q776 -20 696 -20Q524 -20 391 64T185 305T109 668V773Q109 983 182 1144T388 1390T694 1476Q870 1476 1003 1391T1209 1147T1281 774V681ZM1089 775Q1089
1032 987 1171T694 1311Q513 1311 409 1173T301 788V681Q301 431 405 287T696 143T984 278T1089 667V775Z" />
<glyph unicode="R" horiz-adv-x="1261" d="M703 589H361V0H168V1456H650Q896 1456 1028 1344T1161 1018Q1161 882 1088 781T883 630L1225 12V0H1019L703 589ZM361 746H656Q799 746 883 820T968 1018Q968 1153 888 1225T655 1298H361V746Z" />
<glyph unicode="S" horiz-adv-x="1215" d="M598 649Q351 720 239 823T126 1079Q126 1251 263 1363T621 1476Q771 1476 888 1418T1070 1258T1135 1035H942Q942 1167 858 1242T621 1318Q479 1318 400 1256T320 1082Q320 993 395 932T652 819T936 707T1088 563T1138
370Q1138 193 1000 87T631 -20Q481 -20 351 37T151 195T80 422H273Q273 290 370 214T631 137Q783 137 864 199T945 368T870 533T598 649Z" />
<glyph unicode="T" horiz-adv-x="1222" d="M1175 1298H707V0H516V1298H49V1456H1175V1298Z" />
<glyph unicode="U" horiz-adv-x="1328" d="M1194 1456V466Q1193 260 1065 129T716 -18L665 -20Q426 -20 284 109T140 464V1456H330V470Q330 312 417 225T665 137Q828 137 914 224T1001 469V1456H1194Z" />
<glyph unicode="V" horiz-adv-x="1303" d="M651 255L1067 1456H1277L737 0H567L28 1456H237L651 255Z" />
<glyph unicode="W" horiz-adv-x="1817" d="M483 459L511 267L552 440L840 1456H1002L1283 440L1323 264L1354 460L1580 1456H1773L1420 0H1245L945 1061L922 1172L899 1061L588 0H413L61 1456H253L483 459Z" />
<glyph unicode="X" horiz-adv-x="1284" d="M644 898L993 1456H1219L759 734L1230 0H1002L644 568L284 0H57L529 734L68 1456H293L644 898Z" />
<glyph unicode="Y" horiz-adv-x="1230" d="M613 725L993 1456H1211L709 543V0H517V543L15 1456H235L613 725Z" />
<glyph unicode="Z" horiz-adv-x="1226" d="M313 157H1146V0H86V144L884 1298H99V1456H1114V1315L313 157Z" />
<glyph unicode="[" horiz-adv-x="543" d="M523 1512H332V-160H523V-312H146V1664H523V1512Z" />
<glyph unicode="\" horiz-adv-x="840" d="M40 1456H216L824 -125H648L40 1456Z" />
<glyph unicode="]" horiz-adv-x="543" d="M9 1664H387V-312H9V-160H202V1512H9V1664Z" />
<glyph unicode="^" horiz-adv-x="856" d="M426 1211L236 729H64L363 1456H490L788 729H617L426 1211Z" />
<glyph unicode="_" horiz-adv-x="924" d="M920 -151H4V0H920V-151Z" />
<glyph unicode="`" horiz-adv-x="633" d="M474 1242H315L57 1536H280L474 1242Z" />
<glyph unicode="a" horiz-adv-x="1114" d="M808 0Q792 32 782 114Q653 -20 474 -20Q314 -20 212 70T109 300Q109 469 237 562T599 656H779V741Q779 838 721 895T550 953Q451 953 384 903T317 782H131Q131 863 188 938T344 1058T561 1102Q748 1102 854 1009T964
751V253Q964 104 1002 16V0H808ZM501 141Q588 141 666 186T779 303V525H634Q294 525 294 326Q294 239 352 190T501 141Z" />
<glyph unicode="b" horiz-adv-x="1149" d="M1056 529Q1056 281 942 131T636 -20Q431 -20 319 125L310 0H140V1536H325V963Q437 1102 634 1102T943 953T1056 545V529ZM871 550Q871 739 798 842T588 945Q405 945 325 775V307Q410 137 590 137Q723 137 797 240T871 550Z" />
<glyph unicode="c" horiz-adv-x="1072" d="M574 131Q673 131 747 191T829 341H1004Q999 248 940 164T783 30T574 -20Q353 -20 223 127T92 531V562Q92 720 150 843T316 1034T573 1102Q755 1102 875 993T1004 710H829Q821 815 750 882T573 950Q432 950 355 849T277
555V520Q277 333 354 232T574 131Z" />
<glyph unicode="d" horiz-adv-x="1155" d="M95 550Q95 799 213 950T522 1102Q712 1102 823 972V1536H1008V0H838L829 116Q718 -20 520 -20Q332 -20 214 134T95 536V550ZM280 529Q280 345 356 241T566 137Q742 137 823 295V792Q740 945 568 945Q432 945 356 840T280 529Z" />
<glyph unicode="e" horiz-adv-x="1085" d="M589 -20Q369 -20 231 124T93 511V545Q93 706 154 832T326 1030T566 1102Q777 1102 894 963T1011 565V488H278Q282 328 371 230T599 131Q697 131 765 171T884 277L997 189Q861 -20 589 -20ZM566 950Q454 950 378 869T284
640H826V654Q818 795 750 872T566 950Z" />
<glyph unicode="f" horiz-adv-x="711" d="M231 0V939H60V1082H231V1193Q231 1367 324 1462T587 1557Q651 1557 714 1540L704 1390Q657 1399 604 1399Q514 1399 465 1347T416 1196V1082H647V939H416V0H231Z" />
<glyph unicode="g" horiz-adv-x="1149" d="M96 550Q96 803 213 952T523 1102Q721 1102 832 962L841 1082H1010V26Q1010 -184 886 -305T551 -426Q434 -426 322 -376T151 -239L247 -128Q366 -275 538 -275Q673 -275 748 -199T824 15V108Q713 -20 521 -20Q331 -20
214 133T96 550ZM282 529Q282 346 357 242T567 137Q742 137 824 296V790Q739 945 569 945Q434 945 358 840T282 529Z" />
<glyph unicode="h" horiz-adv-x="1128" d="M325 951Q448 1102 645 1102Q988 1102 991 715V0H806V716Q805 833 753 889T589 945Q499 945 431 897T325 771V0H140V1536H325V951Z" />
<glyph unicode="i" horiz-adv-x="497" d="M341 0H156V1082H341V0ZM141 1369Q141 1414 168 1445T250 1476T332 1445T360 1369T332 1294T250 1264T169 1294T141 1369Z" />
<glyph unicode="j" horiz-adv-x="489" d="M331 1082V-125Q331 -437 48 -437Q-13 -437 -65 -419V-271Q-33 -279 19 -279Q81 -279 113 -246T146 -129V1082H331ZM127 1369Q127 1413 154 1444T235 1476Q289 1476 317 1445T345 1369T317 1294T235 1264T154 1294T127 1369Z" />
<glyph unicode="k" horiz-adv-x="1038" d="M442 501L326 380V0H141V1536H326V607L425 726L762 1082H987L566 630L1036 0H819L442 501Z" />
<glyph unicode="l" horiz-adv-x="497" d="M341 0H156V1536H341V0Z" />
<glyph unicode="m" horiz-adv-x="1795" d="M314 1082L319 962Q438 1102 640 1102Q867 1102 949 928Q1003 1006 1089 1054T1294 1102Q1650 1102 1656 725V0H1471V714Q1471 830 1418 887T1240 945Q1137 945 1069 884T990 718V0H804V709Q804 945 573 945Q391 945
324 790V0H139V1082H314Z" />
<glyph unicode="n" horiz-adv-x="1130" d="M315 1082L321 946Q445 1102 645 1102Q988 1102 991 715V0H806V716Q805 833 753 889T589 945Q499 945 431 897T325 771V0H140V1082H315Z" />
<glyph unicode="o" horiz-adv-x="1168" d="M91 551Q91 710 153 837T327 1033T582 1102Q803 1102 939 949T1076 542V529Q1076 371 1016 246T843 50T584 -20Q364 -20 228 133T91 538V551ZM277 529Q277 349 360 240T584 131Q725 131 808 241T891 551Q891 729 807
839T582 950Q445 950 361 841T277 529Z" />
<glyph unicode="p" horiz-adv-x="1149" d="M1054 529Q1054 282 941 131T635 -20Q438 -20 325 105V-416H140V1082H309L318 962Q431 1102 632 1102Q827 1102 940 955T1054 546V529ZM869 550Q869 733 791 839T577 945Q409 945 325 796V279Q408 131 579 131Q712 131
790 236T869 550Z" />
<glyph unicode="q" horiz-adv-x="1164" d="M95 550Q95 805 212 953T526 1102Q718 1102 829 973L837 1082H1007V-416H822V100Q710 -20 524 -20Q328 -20 212 132T95 537V550ZM280 529Q280 343 358 237T570 131Q735 131 822 277V807Q734 950 572 950Q438 950 359
844T280 529Z" />
<glyph unicode="r" horiz-adv-x="693" d="M663 916Q621 923 572 923Q390 923 325 768V0H140V1082H320L323 957Q414 1102 581 1102Q635 1102 663 1088V916Z" />
<glyph unicode="s" horiz-adv-x="1056" d="M770 287Q770 362 714 403T517 475T294 547T172 647T132 785Q132 918 244 1010T532 1102Q716 1102 830 1007T945 764H759Q759 840 695 895T532 950Q431 950 374 906T317 791Q317 724 370 690T561 625T786 551T913 448T955
300Q955 155 839 68T538 -20Q408 -20 308 26T152 154T95 333H280Q285 240 354 186T538 131Q643 131 706 173T770 287Z" />
<glyph unicode="t" horiz-adv-x="669" d="M391 1344V1082H593V939H391V268Q391 203 418 171T510 138Q542 138 598 150V0Q525 -20 456 -20Q332 -20 269 55T206 268V939H9V1082H206V1344H391Z" />
<glyph unicode="u" horiz-adv-x="1129" d="M808 107Q700 -20 491 -20Q318 -20 228 80T136 378V1082H321V383Q321 137 521 137Q733 137 803 295V1082H988V0H812L808 107Z" />
<glyph unicode="v" horiz-adv-x="992" d="M497 251L765 1082H954L566 0H425L33 1082H222L497 251Z" />
<glyph unicode="w" horiz-adv-x="1539" d="M1098 255L1306 1082H1491L1176 0H1026L763 820L507 0H357L43 1082H227L440 272L692 1082H841L1098 255Z" />
<glyph unicode="x" horiz-adv-x="1015" d="M503 687L743 1082H959L605 547L970 0H756L506 405L256 0H41L406 547L52 1082H266L503 687Z" />
<glyph unicode="y" horiz-adv-x="969" d="M494 271L746 1082H944L509 -167Q408 -437 188 -437L153 -434L84 -421V-271L134 -275Q228 -275 280 -237T367 -98L408 12L22 1082H224L494 271Z" />
<glyph unicode="z" horiz-adv-x="1015" d="M314 151H947V0H88V136L685 929H97V1082H917V951L314 151Z" />
<glyph unicode="{" horiz-adv-x="693" d="M632 -366Q455 -316 366 -202T276 101V300Q276 543 64 543V688Q276 688 276 930V1138Q278 1321 365 1433T632 1597L670 1482Q461 1415 461 1133V931Q461 704 294 615Q461 525 461 296V90Q464 -185 670 -251L632 -366Z" />
<glyph unicode="|" horiz-adv-x="499" d="M324 -270H175V1456H324V-270Z" />
<glyph unicode="}" horiz-adv-x="693" d="M19 -251Q222 -186 229 80V300Q229 531 410 615Q229 697 229 930V1133Q229 1415 20 1482L58 1597Q235 1547 324 1435T414 1137V927Q414 688 626 688V543Q414 543 414 300V98Q414 -90 324 -203T58 -366L19 -251Z" />
<glyph unicode="~" horiz-adv-x="1393" d="M1263 777Q1263 619 1170 511T939 402Q867 402 803 428T655 529T533 621T454 639Q376 639 334 586T292 438L131 436Q131 596 223 699T454 802Q530 802 600 770T758 658T910 567L939 565Q1015 565 1062 623T1110 776L1263 777Z" />
<glyph unicode="&#xa0;" horiz-adv-x="507" />
<glyph unicode="&#xa1;" horiz-adv-x="499" d="M170 684H338L351 -360H157L170 684ZM358 996Q358 951 331 920T249 889T167 920T139 996T167 1071T249 1101T330 1071T358 996Z" />
<glyph unicode="&#xa2;" horiz-adv-x="1120" d="M586 131Q686 131 760 191T842 341H1017Q1011 215 912 115T669 -12V-245H484V-11Q305 23 205 165T105 527V562Q105 774 206 916T484 1092V1318H669V1095Q819 1072 915 966T1017 710H842Q834 815 763 882T586 950Q445
950 368 849T290 555V520Q290 333 367 232T586 131Z" />
<glyph unicode="&#xa3;" horiz-adv-x="1190" d="M449 622L457 402Q457 248 395 157H1128L1127 0H95V157H172Q212 166 237 231T264 393V401L256 622H91V779H251L242 1039Q242 1238 364 1357T687 1476Q877 1476 988 1370T1099 1087H908Q908 1194 845 1256T670 1318Q565
1318 500 1241T435 1039L444 779H763V622H449Z" />
<glyph unicode="&#xa4;" horiz-adv-x="1460" d="M1103 112Q944 -20 735 -20Q528 -20 369 110L235 -26L105 109L244 250Q140 406 140 608Q140 814 252 977L105 1128L235 1264L382 1114Q540 1234 735 1234Q931 1234 1090 1113L1239 1265L1371 1128L1220 974Q1330
811 1330 608Q1330 412 1228 253L1371 109L1239 -27L1103 112ZM311 608Q311 485 368 379T524 212T735 151T946 212T1100 379T1157 608Q1157 730 1101 835T946 1001T735 1062Q622 1062 524 1002T369 836T311 608Z" />
<glyph unicode="&#xa5;" horiz-adv-x="1075" d="M539 793L847 1456H1060L693 736H954V611H630V446H954V322H630V0H437V322H119V446H437V611H119V736H382L15 1456H231L539 793Z" />
<glyph unicode="&#xa6;" horiz-adv-x="491" d="M147 -270V521H333V-270H147ZM333 698H147V1456H333V698Z" />
<glyph unicode="&#xa7;" horiz-adv-x="1256" d="M1145 431Q1145 242 959 157Q1028 108 1064 40T1100 -128Q1100 -296 970 -395T612 -495Q500 -495 400 -467T229 -382Q90 -269 90 -64L276 -62Q276 -192 366 -267T612 -343Q748 -343 831 -285T914 -130Q914 -41 843
11T563 126Q381 174 285 229T143 362T96 551Q96 737 278 825Q212 874 177 942T141 1110Q141 1276 274 1376T630 1476Q862 1476 992 1363T1122 1045H937Q937 1170 853 1247T630 1325Q488 1325 408 1268T327 1112Q327 1043 355 1003T450 931T661 858T889 782T1030
698T1116 585T1145 431ZM602 691Q512 715 437 742Q357 723 320 673T282 553Q282 483 309 443T402 370T611 296T797 238Q875 258 917 308T959 428Q959 516 890 570T602 691Z" />
<glyph unicode="&#xa8;" horiz-adv-x="856" d="M101 1371Q101 1416 128 1446T210 1477T292 1447T320 1371T292 1296T210 1266T129 1296T101 1371ZM531 1369Q531 1414 558 1445T640 1476T722 1445T750 1369T722 1294T640 1264T559 1294T531 1369Z" />
<glyph unicode="&#xa9;" horiz-adv-x="1609" d="M1119 597Q1119 444 1033 364T788 283Q631 283 537 388T442 676V786Q442 962 537 1067T788 1173Q948 1173 1034 1091T1120 860H974Q974 959 927 1001T788 1044Q694 1044 640 975T586 783V670Q586 550 640 481T788
412Q880 412 926 454T973 597H1119ZM206 729Q206 557 286 411T503 181T801 98T1098 181T1315 410T1395 729Q1395 899 1316 1044T1100 1272T801 1356Q641 1356 503 1274T286 1045T206 729ZM91 729Q91 931 184 1104T443 1376T801 1476T1158 1377T1416 1104T1510 729Q1510
532 1420 360T1165 84T801 -21Q604 -21 439 82T182 358T91 729Z" />
<glyph unicode="&#xaa;" horiz-adv-x="915" d="M618 705Q606 739 600 777Q524 691 396 691Q277 691 212 753T147 918Q147 1029 230 1089T486 1149H594V1201Q594 1336 470 1336Q401 1336 362 1309T322 1231L161 1243Q161 1346 247 1411T470 1476Q603 1476 680 1405T757
1199V883Q757 786 783 705H618ZM435 828Q478 828 522 848T594 895V1037H482Q399 1036 355 1005T310 922Q310 828 435 828Z" />
<glyph unicode="&#xab;" horiz-adv-x="961" d="M536 804L794 407H653L358 795V814L653 1203H794L536 804ZM610 548L868 151H727L432 539V558L727 947H868L610 548Z" />
<glyph unicode="&#xac;" horiz-adv-x="1134" d="M958 375H772V639H127V800H958V375Z" />
<glyph unicode="&#xad;" horiz-adv-x="565" d="M525 543H37V694H525V543Z" />
<glyph unicode="&#xae;" horiz-adv-x="1610" d="M90 729Q90 931 183 1104T442 1376T800 1476T1157 1377T1415 1104T1509 729Q1509 532 1419 360T1164 84T800 -21Q603 -21 438 82T181 358T90 729ZM205 729Q205 557 285 411T502 181T800 98Q961 98 1099 182T1315
412T1394 729Q1394 900 1316 1044T1099 1272T800 1356Q640 1356 502 1274T285 1045T205 729ZM653 654V316H512V1165H788Q941 1165 1025 1100T1110 909Q1110 786 982 721Q1104 671 1105 517V456Q1105 370 1122 332V316H977Q963 352 963 444T960 554Q944 650 829
654H653ZM653 782H809Q881 784 925 817T969 904Q969 977 930 1007T791 1038H653V782Z" />
<glyph unicode="&#xaf;" horiz-adv-x="938" d="M814 1302H142V1445H814V1302Z" />
<glyph unicode="&#xb0;" horiz-adv-x="765" d="M130 1216Q130 1320 204 1398T385 1476Q489 1476 562 1399T636 1216Q636 1110 563 1035T385 960Q280 960 205 1035T130 1216ZM385 1088Q439 1088 476 1123T513 1216Q513 1274 476 1311T385 1349Q330 1349 293 1310T255
1216T292 1125T385 1088Z" />
<glyph unicode="&#xb1;" horiz-adv-x="1094" d="M649 854H1013V703H649V289H482V703H97V854H482V1267H649V854ZM970 0H135V152H970V0Z" />
<glyph unicode="&#xb2;" horiz-adv-x="751" d="M683 667H84V775L384 1057Q493 1159 493 1228Q493 1277 461 1307T369 1338Q294 1338 259 1300T223 1205H66Q66 1319 149 1393T365 1467T574 1404T651 1230Q651 1126 544 1019L460 940L284 795H683V667Z" />
<glyph unicode="&#xb3;" horiz-adv-x="751" d="M265 1126H349Q423 1126 459 1156T495 1235Q495 1280 464 1309T362 1338Q305 1338 268 1313T230 1246H73Q73 1344 154 1405T360 1467Q497 1467 575 1407T653 1242Q653 1187 618 1142T517 1071Q666 1030 666 887Q666
781 581 719T360 656Q228 656 145 719T62 889H220Q220 844 259 814T366 784Q436 784 472 814T509 895Q509 1008 353 1010H265V1126Z" />
<glyph unicode="&#xb4;" horiz-adv-x="642" d="M316 1536H540L272 1242H123L316 1536Z" />
<glyph unicode="&#xb5;" horiz-adv-x="1160" d="M339 1082V449Q340 286 391 208T559 130Q758 130 820 282V1082H1006V0H839L830 115Q737 -20 567 -20Q420 -20 339 53V-416H154V1082H339Z" />
<glyph unicode="&#xb6;" horiz-adv-x="1001" d="M646 0V520H562Q332 520 200 647T67 988Q67 1201 200 1328T563 1456H832V0H646Z" />
<glyph unicode="&#xb7;" horiz-adv-x="534" d="M147 729Q147 777 175 809T261 841T347 809T377 729Q377 682 348 651T261 619T176 650T147 729Z" />
<glyph unicode="&#xb8;" horiz-adv-x="507" d="M285 0L273 -52Q426 -79 426 -225Q426 -322 346 -378T123 -435L116 -328Q195 -328 238 -302T282 -229Q282 -185 250 -164T120 -134L152 0H285Z" />
<glyph unicode="&#xb9;" horiz-adv-x="751" d="M495 667H338V1268L122 1211V1339L477 1456H495V667Z" />
<glyph unicode="&#xba;" horiz-adv-x="931" d="M122 1123Q122 1281 216 1378T464 1476Q619 1476 713 1380T807 1117V1043Q807 884 714 787T466 690T217 787T122 1049V1123ZM285 1043Q285 943 333 886T466 829Q549 829 596 886T644 1045V1123Q644 1222 596 1279T464
1336Q383 1336 335 1281T285 1129V1043Z" />
<glyph unicode="&#xbb;" horiz-adv-x="960" d="M244 949L539 560V541L244 152H102L360 550L102 949H244ZM593 949L888 560V541L593 152H451L709 550L451 949H593Z" />
<glyph unicode="&#xbc;" horiz-adv-x="1500" d="M458 664H301V1265L85 1208V1336L440 1453H458V664ZM443 118L339 184L1050 1322L1154 1256L443 118ZM1318 299H1425V169H1318V0H1161V169H786L780 271L1157 789H1318V299ZM938 299H1161V588L1144 560L938 299Z" />
<glyph unicode="&#xbd;" horiz-adv-x="1589" d="M399 118L295 184L1006 1322L1110 1256L399 118ZM453 664H296V1265L80 1208V1336L435 1453H453V664ZM1481 0H882V108L1182 390Q1291 492 1291 561Q1291 610 1259 640T1167 671Q1092 671 1057 633T1021 538H864Q864
652 947 726T1163 800T1372 737T1449 563Q1449 459 1342 352L1258 273L1082 128H1481V0Z" />
<glyph unicode="&#xbe;" horiz-adv-x="1593" d="M570 118L466 184L1177 1322L1281 1256L570 118ZM1410 299H1517V169H1410V0H1253V169H878L872 271L1249 789H1410V299ZM1030 299H1253V588L1236 560L1030 299ZM314 1126H398Q472 1126 508 1156T544 1235Q544 1280
513 1309T411 1338Q354 1338 317 1313T279 1246H122Q122 1344 203 1405T409 1467Q546 1467 624 1407T702 1242Q702 1187 667 1142T566 1071Q715 1030 715 887Q715 781 630 719T409 656Q277 656 194 719T111 889H269Q269 844 308 814T415 784Q485 784 521 814T558
895Q558 1008 402 1010H314V1126Z" />
<glyph unicode="&#xbf;" horiz-adv-x="969" d="M588 680Q587 574 567 511T498 388T358 233T255 37L253 0Q253 -109 311 -166T478 -224Q578 -224 640 -168T703 -20H888Q886 -181 774 -283T478 -385Q282 -385 175 -285T68 -5Q68 168 228 343L337 456Q403 534 403
680H588ZM596 997Q596 952 569 921T487 890T405 921T377 997Q377 1041 405 1071T487 1101T568 1071T596 997Z" />
<glyph unicode="&#xc0;" horiz-adv-x="1336" d="M973 380H363L226 0H28L584 1456H752L1309 0H1112L973 380ZM421 538H916L668 1219L421 538ZM778 1552H619L361 1846H584L778 1552Z" />
<glyph unicode="&#xc1;" horiz-adv-x="1336" d="M973 380H363L226 0H28L584 1456H752L1309 0H1112L973 380ZM421 538H916L668 1219L421 538ZM763 1846H987L719 1552H570L763 1846Z" />
<glyph unicode="&#xc2;" horiz-adv-x="1336" d="M973 380H363L226 0H28L584 1456H752L1309 0H1112L973 380ZM421 538H916L668 1219L421 538ZM975 1572V1562H822L672 1732L523 1562H370V1574L616 1846H728L975 1572Z" />
<glyph unicode="&#xc3;" horiz-adv-x="1336" d="M973 380H363L226 0H28L584 1456H752L1309 0H1112L973 380ZM421 538H916L668 1219L421 538ZM1027 1814Q1027 1706 966 1639T812 1572Q771 1572 741 1582T663 1623T593 1660T543 1667Q502 1667 473 1636T444 1555L320
1562Q320 1669 380 1739T534 1809Q569 1809 597 1799T673 1760T746 1722T803 1713Q846 1713 874 1747T903 1826L1027 1814Z" />
<glyph unicode="&#xc4;" horiz-adv-x="1336" d="M973 380H363L226 0H28L584 1456H752L1309 0H1112L973 380ZM421 538H916L668 1219L421 538ZM350 1681Q350 1726 377 1756T459 1787T541 1757T569 1681T541 1606T459 1576T378 1606T350 1681ZM780 1679Q780 1724
807 1755T889 1786T971 1755T999 1679T971 1604T889 1574T808 1604T780 1679Z" />
<glyph unicode="&#xc5;" horiz-adv-x="1336" d="M973 380H363L226 0H28L584 1456H752L1309 0H1112L973 380ZM421 538H916L668 1219L421 538ZM887 1729Q887 1642 825 1584T672 1525Q580 1525 519 1584T457 1729T518 1876T672 1937T825 1876T887 1729ZM556 1729Q556
1682 589 1648T672 1614Q720 1614 754 1647T788 1729T755 1812T672 1847Q622 1847 589 1812T556 1729Z" />
<glyph unicode="&#xc6;" horiz-adv-x="1914" d="M1879 0H996L981 353H417L212 0H-14L866 1456H1817V1304H1126L1146 833H1736V682H1152L1174 151H1879V0ZM518 527H974L943 1260L518 527Z" />
<glyph unicode="&#xc7;" horiz-adv-x="1333" d="M1240 462Q1213 231 1070 106T688 -20Q430 -20 275 165T119 660V800Q119 1003 191 1157T397 1393T705 1476Q937 1476 1077 1347T1240 988H1047Q1022 1162 939 1240T705 1318Q521 1318 417 1182T312 795V654Q312
417 411 277T688 137Q848 137 933 209T1047 462H1240ZM751 -9L739 -61Q892 -88 892 -234Q892 -331 812 -387T589 -444L582 -337Q661 -337 704 -311T748 -238Q748 -194 716 -173T586 -143L618 -9H751Z" />
<glyph unicode="&#xc8;" horiz-adv-x="1164" d="M992 673H361V157H1094V0H169V1456H1084V1298H361V830H992V673ZM725 1564H566L308 1858H531L725 1564Z" />
<glyph unicode="&#xc9;" horiz-adv-x="1164" d="M992 673H361V157H1094V0H169V1456H1084V1298H361V830H992V673ZM710 1858H934L666 1564H517L710 1858Z" />
<glyph unicode="&#xca;" horiz-adv-x="1164" d="M992 673H361V157H1094V0H169V1456H1084V1298H361V830H992V673ZM922 1584V1574H769L619 1744L470 1574H317V1586L563 1858H675L922 1584Z" />
<glyph unicode="&#xcb;" horiz-adv-x="1164" d="M992 673H361V157H1094V0H169V1456H1084V1298H361V830H992V673ZM297 1693Q297 1738 324 1768T406 1799T488 1769T516 1693T488 1618T406 1588T325 1618T297 1693ZM727 1691Q727 1736 754 1767T836 1798T918 1767T946
1691T918 1616T836 1586T755 1616T727 1691Z" />
<glyph unicode="&#xcc;" horiz-adv-x="557" d="M375 0H183V1456H375V0ZM385 1564H226L-32 1858H191L385 1564Z" />
<glyph unicode="&#xcd;" horiz-adv-x="557" d="M375 0H183V1456H375V0ZM369 1858H593L325 1564H176L369 1858Z" />
<glyph unicode="&#xce;" horiz-adv-x="557" d="M375 0H183V1456H375V0ZM582 1584V1574H429L279 1744L130 1574H-23V1586L223 1858H335L582 1584Z" />
<glyph unicode="&#xcf;" horiz-adv-x="557" d="M375 0H183V1456H375V0ZM-43 1693Q-43 1738 -16 1768T66 1799T148 1769T176 1693T148 1618T66 1588T-15 1618T-43 1693ZM387 1691Q387 1736 414 1767T496 1798T578 1767T606 1691T578 1616T496 1586T415 1616T387 1691Z" />
<glyph unicode="&#xd0;" horiz-adv-x="1373" d="M199 0V666H7V817H199V1456H610Q800 1456 946 1372T1171 1133T1252 777V684Q1252 478 1173 323T946 85T602 0H199ZM643 666H391V157H592Q814 157 937 294T1062 680V773Q1062 1021 946 1158T615 1298H391V817H643V666Z" />
<glyph unicode="&#xd1;" horiz-adv-x="1460" d="M1288 0H1095L362 1122V0H169V1456H362L1097 329V1456H1288V0ZM1081 1814Q1081 1706 1020 1639T866 1572Q825 1572 795 1582T717 1623T647 1660T597 1667Q556 1667 527 1636T498 1555L374 1562Q374 1669 434 1739T588
1809Q623 1809 651 1799T727 1760T800 1722T857 1713Q900 1713 928 1747T957 1826L1081 1814Z" />
<glyph unicode="&#xd2;" horiz-adv-x="1408" d="M1289 681Q1289 467 1217 308T1013 64T705 -20Q533 -20 400 64T194 305T118 668V773Q118 983 191 1144T397 1390T703 1476Q878 1476 1011 1392T1217 1147T1289 773V681ZM1098 775Q1098 1034 994 1172T703 1311Q521
1311 417 1173T309 788V681Q309 430 414 287T705 143Q891 143 993 278T1098 667V775ZM812 1554H653L395 1848H618L812 1554Z" />
<glyph unicode="&#xd3;" horiz-adv-x="1408" d="M1289 681Q1289 467 1217 308T1013 64T705 -20Q533 -20 400 64T194 305T118 668V773Q118 983 191 1144T397 1390T703 1476Q878 1476 1011 1392T1217 1147T1289 773V681ZM1098 775Q1098 1034 994 1172T703 1311Q521
1311 417 1173T309 788V681Q309 430 414 287T705 143Q891 143 993 278T1098 667V775ZM797 1848H1021L753 1554H604L797 1848Z" />
<glyph unicode="&#xd4;" horiz-adv-x="1408" d="M1289 681Q1289 467 1217 308T1013 64T705 -20Q533 -20 400 64T194 305T118 668V773Q118 983 191 1144T397 1390T703 1476Q878 1476 1011 1392T1217 1147T1289 773V681ZM1098 775Q1098 1034 994 1172T703 1311Q521
1311 417 1173T309 788V681Q309 430 414 287T705 143Q891 143 993 278T1098 667V775ZM1009 1574V1564H856L706 1734L557 1564H404V1576L650 1848H762L1009 1574Z" />
<glyph unicode="&#xd5;" horiz-adv-x="1408" d="M1289 681Q1289 467 1217 308T1013 64T705 -20Q533 -20 400 64T194 305T118 668V773Q118 983 191 1144T397 1390T703 1476Q878 1476 1011 1392T1217 1147T1289 773V681ZM1098 775Q1098 1034 994 1172T703 1311Q521
1311 417 1173T309 788V681Q309 430 414 287T705 143Q891 143 993 278T1098 667V775ZM1061 1816Q1061 1708 1000 1641T846 1574Q805 1574 775 1584T697 1625T627 1662T577 1669Q536 1669 507 1638T478 1557L354 1564Q354 1671 414 1741T568 1811Q603 1811 631 1801T707
1762T780 1724T837 1715Q880 1715 908 1749T937 1828L1061 1816Z" />
<glyph unicode="&#xd6;" horiz-adv-x="1408" d="M1289 681Q1289 467 1217 308T1013 64T705 -20Q533 -20 400 64T194 305T118 668V773Q118 983 191 1144T397 1390T703 1476Q878 1476 1011 1392T1217 1147T1289 773V681ZM1098 775Q1098 1034 994 1172T703 1311Q521
1311 417 1173T309 788V681Q309 430 414 287T705 143Q891 143 993 278T1098 667V775ZM384 1683Q384 1728 411 1758T493 1789T575 1759T603 1683T575 1608T493 1578T412 1608T384 1683ZM814 1681Q814 1726 841 1757T923 1788T1005 1757T1033 1681T1005 1606T923
1576T842 1606T814 1681Z" />
<glyph unicode="&#xd7;" horiz-adv-x="1092" d="M89 329L419 665L91 1000L210 1123L539 788L868 1123L987 1000L659 665L989 329L870 206L539 543L208 206L89 329Z" />
<glyph unicode="&#xd8;" horiz-adv-x="1408" d="M1289 681Q1289 467 1217 308T1013 64T705 -20Q534 -20 403 62L306 -93H164L308 138Q118 330 118 690V773Q118 983 191 1144T397 1390T703 1476Q917 1476 1065 1351L1168 1516H1309L1150 1261Q1287 1074 1289 780V681ZM309
681Q309 437 407 296L971 1200Q869 1311 703 1311Q521 1311 417 1173T309 788V681ZM1098 775Q1098 957 1042 1088L493 207Q584 143 705 143Q891 143 993 278T1098 667V775Z" />
<glyph unicode="&#xd9;" horiz-adv-x="1328" d="M1194 1456V466Q1193 260 1065 129T716 -18L665 -20Q426 -20 284 109T140 464V1456H330V470Q330 312 417 225T665 137Q828 137 914 224T1001 469V1456H1194ZM773 1552H614L356 1846H579L773 1552Z" />
<glyph unicode="&#xda;" horiz-adv-x="1328" d="M1194 1456V466Q1193 260 1065 129T716 -18L665 -20Q426 -20 284 109T140 464V1456H330V470Q330 312 417 225T665 137Q828 137 914 224T1001 469V1456H1194ZM758 1846H982L714 1552H565L758 1846Z" />
<glyph unicode="&#xdb;" horiz-adv-x="1328" d="M1194 1456V466Q1193 260 1065 129T716 -18L665 -20Q426 -20 284 109T140 464V1456H330V470Q330 312 417 225T665 137Q828 137 914 224T1001 469V1456H1194ZM970 1572V1562H817L667 1732L518 1562H365V1574L611
1846H723L970 1572Z" />
<glyph unicode="&#xdc;" horiz-adv-x="1328" d="M1194 1456V466Q1193 260 1065 129T716 -18L665 -20Q426 -20 284 109T140 464V1456H330V470Q330 312 417 225T665 137Q828 137 914 224T1001 469V1456H1194ZM345 1681Q345 1726 372 1756T454 1787T536 1757T564
1681T536 1606T454 1576T373 1606T345 1681ZM775 1679Q775 1724 802 1755T884 1786T966 1755T994 1679T966 1604T884 1574T803 1604T775 1679Z" />
<glyph unicode="&#xdd;" horiz-adv-x="1230" d="M613 725L993 1456H1211L709 543V0H517V543L15 1456H235L613 725ZM708 1846H932L664 1552H515L708 1846Z" />
<glyph unicode="&#xde;" horiz-adv-x="1210" d="M352 1456V1163H631Q778 1163 888 1111T1057 961T1117 738Q1117 544 985 429T626 313H352V0H166V1456H352ZM352 1011V465H629Q771 465 851 540T931 736Q931 859 851 934T635 1011H352Z" />
<glyph unicode="&#xdf;" horiz-adv-x="1218" d="M324 0H139V1111Q139 1319 242 1436T532 1554Q712 1554 810 1465T909 1216Q909 1091 845 990T781 819Q781 768 818 721T950 601T1087 461T1130 317Q1130 158 1029 69T745 -20Q664 -20 574 2T445 52L488 207Q537
175 604 153T725 131Q832 131 888 178T945 307Q945 359 908 407T777 528T639 671T595 821Q595 910 664 1013T734 1201Q734 1295 682 1348T542 1402Q324 1402 324 1109V0Z" />
<glyph unicode="&#xe0;" horiz-adv-x="1114" d="M808 0Q792 32 782 114Q653 -20 474 -20Q314 -20 212 70T109 300Q109 469 237 562T599 656H779V741Q779 838 721 895T550 953Q451 953 384 903T317 782H131Q131 863 188 938T344 1058T561 1102Q748 1102 854 1009T964
751V253Q964 104 1002 16V0H808ZM501 141Q588 141 666 186T779 303V525H634Q294 525 294 326Q294 239 352 190T501 141ZM687 1242H528L270 1536H493L687 1242Z" />
<glyph unicode="&#xe1;" horiz-adv-x="1114" d="M808 0Q792 32 782 114Q653 -20 474 -20Q314 -20 212 70T109 300Q109 469 237 562T599 656H779V741Q779 838 721 895T550 953Q451 953 384 903T317 782H131Q131 863 188 938T344 1058T561 1102Q748 1102 854 1009T964
751V253Q964 104 1002 16V0H808ZM501 141Q588 141 666 186T779 303V525H634Q294 525 294 326Q294 239 352 190T501 141ZM672 1536H896L628 1242H479L672 1536Z" />
<glyph unicode="&#xe2;" horiz-adv-x="1114" d="M808 0Q792 32 782 114Q653 -20 474 -20Q314 -20 212 70T109 300Q109 469 237 562T599 656H779V741Q779 838 721 895T550 953Q451 953 384 903T317 782H131Q131 863 188 938T344 1058T561 1102Q748 1102 854 1009T964
751V253Q964 104 1002 16V0H808ZM501 141Q588 141 666 186T779 303V525H634Q294 525 294 326Q294 239 352 190T501 141ZM884 1262V1252H731L581 1422L432 1252H279V1264L525 1536H637L884 1262Z" />
<glyph unicode="&#xe3;" horiz-adv-x="1114" d="M808 0Q792 32 782 114Q653 -20 474 -20Q314 -20 212 70T109 300Q109 469 237 562T599 656H779V741Q779 838 721 895T550 953Q451 953 384 903T317 782H131Q131 863 188 938T344 1058T561 1102Q748 1102 854 1009T964
751V253Q964 104 1002 16V0H808ZM501 141Q588 141 666 186T779 303V525H634Q294 525 294 326Q294 239 352 190T501 141ZM936 1504Q936 1396 875 1329T721 1262Q680 1262 650 1272T572 1313T502 1350T452 1357Q411 1357 382 1326T353 1245L229 1252Q229 1359 289
1429T443 1499Q478 1499 506 1489T582 1450T655 1412T712 1403Q755 1403 783 1437T812 1516L936 1504Z" />
<glyph unicode="&#xe4;" horiz-adv-x="1114" d="M808 0Q792 32 782 114Q653 -20 474 -20Q314 -20 212 70T109 300Q109 469 237 562T599 656H779V741Q779 838 721 895T550 953Q451 953 384 903T317 782H131Q131 863 188 938T344 1058T561 1102Q748 1102 854 1009T964
751V253Q964 104 1002 16V0H808ZM501 141Q588 141 666 186T779 303V525H634Q294 525 294 326Q294 239 352 190T501 141ZM259 1371Q259 1416 286 1446T368 1477T450 1447T478 1371T450 1296T368 1266T287 1296T259 1371ZM689 1369Q689 1414 716 1445T798 1476T880
1445T908 1369T880 1294T798 1264T717 1294T689 1369Z" />
<glyph unicode="&#xe5;" horiz-adv-x="1114" d="M808 0Q792 32 782 114Q653 -20 474 -20Q314 -20 212 70T109 300Q109 469 237 562T599 656H779V741Q779 838 721 895T550 953Q451 953 384 903T317 782H131Q131 863 188 938T344 1058T561 1102Q748 1102 854 1009T964
751V253Q964 104 1002 16V0H808ZM501 141Q588 141 666 186T779 303V525H634Q294 525 294 326Q294 239 352 190T501 141ZM796 1419Q796 1332 734 1274T581 1215Q489 1215 428 1274T366 1419T427 1566T581 1627T734 1566T796 1419ZM465 1419Q465 1372 498 1338T581
1304Q629 1304 663 1337T697 1419T664 1502T581 1537Q531 1537 498 1502T465 1419Z" />
<glyph unicode="&#xe6;" horiz-adv-x="1729" d="M1262 -20Q1001 -20 865 160Q800 74 687 27T433 -20Q266 -20 172 66T78 304Q78 461 191 548T526 635H749V720Q749 827 694 888T535 950Q430 950 360 895T290 759L106 778Q106 921 227 1011T535 1102Q650 1102 738
1061T876 936Q939 1015 1026 1058T1218 1102Q1428 1102 1544 974T1660 612V497H932Q939 321 1026 226T1262 130Q1410 130 1531 206L1578 237L1642 101Q1484 -20 1262 -20ZM469 130Q541 130 620 167T749 258V495H521Q404 493 334 438T264 300Q264 223 317 177T469
130ZM1218 950Q1103 950 1029 865T937 640H1475V671Q1475 803 1408 876T1218 950Z" />
<glyph unicode="&#xe7;" horiz-adv-x="1072" d="M574 131Q673 131 747 191T829 341H1004Q999 248 940 164T783 30T574 -20Q353 -20 223 127T92 531V562Q92 720 150 843T316 1034T573 1102Q755 1102 875 993T1004 710H829Q821 815 750 882T573 950Q432 950 355
849T277 555V520Q277 333 354 232T574 131ZM604 -9L592 -61Q745 -88 745 -234Q745 -331 665 -387T442 -444L435 -337Q514 -337 557 -311T601 -238Q601 -194 569 -173T439 -143L471 -9H604Z" />
<glyph unicode="&#xe8;" horiz-adv-x="1085" d="M589 -20Q369 -20 231 124T93 511V545Q93 706 154 832T326 1030T566 1102Q777 1102 894 963T1011 565V488H278Q282 328 371 230T599 131Q697 131 765 171T884 277L997 189Q861 -20 589 -20ZM566 950Q454 950 378
869T284 640H826V654Q818 795 750 872T566 950ZM671 1242H512L254 1536H477L671 1242Z" />
<glyph unicode="&#xe9;" horiz-adv-x="1085" d="M589 -20Q369 -20 231 124T93 511V545Q93 706 154 832T326 1030T566 1102Q777 1102 894 963T1011 565V488H278Q282 328 371 230T599 131Q697 131 765 171T884 277L997 189Q861 -20 589 -20ZM566 950Q454 950 378
869T284 640H826V654Q818 795 750 872T566 950ZM656 1536H880L612 1242H463L656 1536Z" />
<glyph unicode="&#xea;" horiz-adv-x="1085" d="M589 -20Q369 -20 231 124T93 511V545Q93 706 154 832T326 1030T566 1102Q777 1102 894 963T1011 565V488H278Q282 328 371 230T599 131Q697 131 765 171T884 277L997 189Q861 -20 589 -20ZM566 950Q454 950 378
869T284 640H826V654Q818 795 750 872T566 950ZM868 1262V1252H715L565 1422L416 1252H263V1264L509 1536H621L868 1262Z" />
<glyph unicode="&#xeb;" horiz-adv-x="1085" d="M589 -20Q369 -20 231 124T93 511V545Q93 706 154 832T326 1030T566 1102Q777 1102 894 963T1011 565V488H278Q282 328 371 230T599 131Q697 131 765 171T884 277L997 189Q861 -20 589 -20ZM566 950Q454 950 378
869T284 640H826V654Q818 795 750 872T566 950ZM243 1371Q243 1416 270 1446T352 1477T434 1447T462 1371T434 1296T352 1266T271 1296T243 1371ZM673 1369Q673 1414 700 1445T782 1476T864 1445T892 1369T864 1294T782 1264T701 1294T673 1369Z" />
<glyph unicode="&#xec;" horiz-adv-x="506" d="M341 0H155V1082H341V0ZM615 1497H456L198 1791H421L615 1497Z" />
<glyph unicode="&#xed;" horiz-adv-x="506" d="M341 0H155V1082H341V0ZM343 1791H567L299 1497H150L343 1791Z" />
<glyph unicode="&#xee;" horiz-adv-x="506" d="M341 0H155V1082H341V0ZM556 1261V1251H403L253 1421L104 1251H-49V1263L197 1535H309L556 1261Z" />
<glyph unicode="&#xef;" horiz-adv-x="506" d="M341 0H155V1082H341V0ZM-69 1370Q-69 1415 -42 1445T40 1476T122 1446T150 1370T122 1295T40 1265T-41 1295T-69 1370ZM361 1368Q361 1413 388 1444T470 1475T552 1444T580 1368T552 1293T470 1263T389 1293T361 1368Z" />
<glyph unicode="&#xf0;" horiz-adv-x="1200" d="M820 1301Q1069 1037 1069 628V535Q1069 377 1011 251T844 52T602 -20Q467 -20 357 44T187 221T126 467Q126 614 182 730T341 912T574 977Q737 977 858 863Q810 1058 669 1199L451 1051L378 1150L570 1281Q438 1372
255 1421L312 1580Q551 1526 726 1387L915 1516L988 1416L820 1301ZM884 635L882 691Q849 752 780 788T618 825Q473 825 392 730T311 467Q311 327 394 229T606 131Q731 131 807 244T884 541V635Z" />
<glyph unicode="&#xf1;" horiz-adv-x="1130" d="M315 1082L321 946Q445 1102 645 1102Q988 1102 991 715V0H806V716Q805 833 753 889T589 945Q499 945 431 897T325 771V0H140V1082H315ZM927 1504Q927 1396 866 1329T712 1262Q671 1262 641 1272T563 1313T493 1350T443
1357Q402 1357 373 1326T344 1245L220 1252Q220 1359 280 1429T434 1499Q469 1499 497 1489T573 1450T646 1412T703 1403Q746 1403 774 1437T803 1516L927 1504Z" />
<glyph unicode="&#xf2;" horiz-adv-x="1168" d="M91 551Q91 710 153 837T327 1033T582 1102Q803 1102 939 949T1076 542V529Q1076 371 1016 246T843 50T584 -20Q364 -20 228 133T91 538V551ZM277 529Q277 349 360 240T584 131Q725 131 808 241T891 551Q891 729
807 839T582 950Q445 950 361 841T277 529ZM681 1242H522L264 1536H487L681 1242Z" />
<glyph unicode="&#xf3;" horiz-adv-x="1168" d="M91 551Q91 710 153 837T327 1033T582 1102Q803 1102 939 949T1076 542V529Q1076 371 1016 246T843 50T584 -20Q364 -20 228 133T91 538V551ZM277 529Q277 349 360 240T584 131Q725 131 808 241T891 551Q891 729
807 839T582 950Q445 950 361 841T277 529ZM666 1536H890L622 1242H473L666 1536Z" />
<glyph unicode="&#xf4;" horiz-adv-x="1168" d="M91 551Q91 710 153 837T327 1033T582 1102Q803 1102 939 949T1076 542V529Q1076 371 1016 246T843 50T584 -20Q364 -20 228 133T91 538V551ZM277 529Q277 349 360 240T584 131Q725 131 808 241T891 551Q891 729
807 839T582 950Q445 950 361 841T277 529ZM878 1262V1252H725L575 1422L426 1252H273V1264L519 1536H631L878 1262Z" />
<glyph unicode="&#xf5;" horiz-adv-x="1168" d="M91 551Q91 710 153 837T327 1033T582 1102Q803 1102 939 949T1076 542V529Q1076 371 1016 246T843 50T584 -20Q364 -20 228 133T91 538V551ZM277 529Q277 349 360 240T584 131Q725 131 808 241T891 551Q891 729
807 839T582 950Q445 950 361 841T277 529ZM930 1504Q930 1396 869 1329T715 1262Q674 1262 644 1272T566 1313T496 1350T446 1357Q405 1357 376 1326T347 1245L223 1252Q223 1359 283 1429T437 1499Q472 1499 500 1489T576 1450T649 1412T706 1403Q749 1403 777
1437T806 1516L930 1504Z" />
<glyph unicode="&#xf6;" horiz-adv-x="1168" d="M91 551Q91 710 153 837T327 1033T582 1102Q803 1102 939 949T1076 542V529Q1076 371 1016 246T843 50T584 -20Q364 -20 228 133T91 538V551ZM277 529Q277 349 360 240T584 131Q725 131 808 241T891 551Q891 729
807 839T582 950Q445 950 361 841T277 529ZM253 1371Q253 1416 280 1446T362 1477T444 1447T472 1371T444 1296T362 1266T281 1296T253 1371ZM683 1369Q683 1414 710 1445T792 1476T874 1445T902 1369T874 1294T792 1264T711 1294T683 1369Z" />
<glyph unicode="&#xf7;" horiz-adv-x="1169" d="M1069 600H71V784H1069V600ZM461 1098Q461 1146 489 1178T575 1210T661 1178T691 1098Q691 1051 662 1020T575 989T490 1020T461 1098ZM461 281Q461 329 489 361T575 393T661 361T691 281Q691 235 662 204T575 172T490
203T461 281Z" />
<glyph unicode="&#xf8;" horiz-adv-x="1160" d="M91 551Q91 710 152 836T326 1032T582 1102Q692 1102 786 1060L859 1208H983L881 1003Q1076 849 1076 529Q1076 371 1014 244T840 49T584 -20Q480 -20 394 15L320 -134H196L296 69Q91 218 91 551ZM276 529Q276 335
373 224L716 918Q654 950 582 950Q444 950 360 841T276 529ZM890 551Q890 733 803 844L463 156Q518 131 584 131Q723 131 806 240T890 535V551Z" />
<glyph unicode="&#xf9;" horiz-adv-x="1129" d="M808 107Q700 -20 491 -20Q318 -20 228 80T136 378V1082H321V383Q321 137 521 137Q733 137 803 295V1082H988V0H812L808 107ZM673 1242H514L256 1536H479L673 1242Z" />
<glyph unicode="&#xfa;" horiz-adv-x="1129" d="M808 107Q700 -20 491 -20Q318 -20 228 80T136 378V1082H321V383Q321 137 521 137Q733 137 803 295V1082H988V0H812L808 107ZM658 1536H882L614 1242H465L658 1536Z" />
<glyph unicode="&#xfb;" horiz-adv-x="1129" d="M808 107Q700 -20 491 -20Q318 -20 228 80T136 378V1082H321V383Q321 137 521 137Q733 137 803 295V1082H988V0H812L808 107ZM870 1262V1252H717L567 1422L418 1252H265V1264L511 1536H623L870 1262Z" />
<glyph unicode="&#xfc;" horiz-adv-x="1129" d="M808 107Q700 -20 491 -20Q318 -20 228 80T136 378V1082H321V383Q321 137 521 137Q733 137 803 295V1082H988V0H812L808 107ZM245 1371Q245 1416 272 1446T354 1477T436 1447T464 1371T436 1296T354 1266T273 1296T245
1371ZM675 1369Q675 1414 702 1445T784 1476T866 1445T894 1369T866 1294T784 1264T703 1294T675 1369Z" />
<glyph unicode="&#xfd;" horiz-adv-x="969" d="M494 271L746 1082H944L509 -167Q408 -437 188 -437L153 -434L84 -421V-271L134 -275Q228 -275 280 -237T367 -98L408 12L22 1082H224L494 271ZM599 1536H823L555 1242H406L599 1536Z" />
<glyph unicode="&#xfe;" horiz-adv-x="1180" d="M1063 529Q1063 282 950 131T644 -20Q447 -20 334 105V-416H149V1536H334V970Q447 1102 641 1102Q836 1102 949 955T1063 546V529ZM878 550Q878 733 800 839T586 945Q418 945 334 796V279Q417 131 588 131Q721 131
799 236T878 550Z" />
<glyph unicode="&#xff;" horiz-adv-x="969" d="M494 271L746 1082H944L509 -167Q408 -437 188 -437L153 -434L84 -421V-271L134 -275Q228 -275 280 -237T367 -98L408 12L22 1082H224L494 271ZM186 1371Q186 1416 213 1446T295 1477T377 1447T405 1371T377 1296T295
1266T214 1296T186 1371ZM616 1369Q616 1414 643 1445T725 1476T807 1445T835 1369T807 1294T725 1264T644 1294T616 1369Z" />
<glyph unicode="&#x2013;" horiz-adv-x="1344" d="M1421 651H419V802H1421V651Z" />
<glyph unicode="&#x2014;" horiz-adv-x="1599" d="M1737 651H401V802H1737V651Z" />
<glyph unicode="&#x2018;" horiz-adv-x="409" d="M270 1555L376 1483Q283 1356 280 1209V1073H96V1189Q96 1291 144 1391T270 1555Z" />
<glyph unicode="&#x2019;" horiz-adv-x="409" d="M153 1046L48 1118Q141 1248 144 1392V1536H327V1406Q326 1306 278 1207T153 1046Z" />
<glyph unicode="&#x201a;" horiz-adv-x="407" d="M141 -283L36 -210Q127 -83 130 63V181H315V81Q315 -20 266 -121T141 -283Z" />
<glyph unicode="&#x201c;" horiz-adv-x="724" d="M278 1555L384 1483Q291 1356 288 1209V1073H104V1189Q104 1291 152 1391T278 1555ZM593 1555L699 1483Q606 1356 603 1209V1073H419V1189Q419 1291 467 1391T593 1555Z" />
<glyph unicode="&#x201d;" horiz-adv-x="731" d="M165 1046L60 1118Q153 1248 156 1392V1536H339V1406Q338 1306 290 1207T165 1046ZM472 1046L367 1118Q460 1248 463 1392V1536H646V1406Q645 1306 597 1207T472 1046Z" />
<glyph unicode="&#x201e;" horiz-adv-x="705" d="M141 -301L36 -229Q127 -92 130 61V246H315V82Q315 -26 266 -131T141 -301ZM437 -301L332 -229Q423 -92 426 61V246H612V82Q612 -25 564 -129T437 -301Z" />
<glyph unicode="&#x2022;" horiz-adv-x="690" d="M138 772Q138 859 193 915T341 971Q432 971 489 917T546 769V732Q546 645 491 590T342 535Q249 535 194 590T138 734V772Z" />
<glyph unicode="&#x2039;" horiz-adv-x="614" d="M286 550L544 153H403L108 541V560L403 949H544L286 550Z" />
<glyph unicode="&#x203a;" horiz-adv-x="614" d="M231 949L526 560V541L231 152H89L347 550L89 949H231Z" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,312 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<defs >
<font id="Roboto" horiz-adv-x="1135" ><font-face
font-family="Roboto Light"
units-per-em="2048"
panose-1="2 0 0 0 0 0 0 0 0 0"
ascent="1900"
descent="-500"
alphabetic="0" />
<glyph unicode=" " horiz-adv-x="498" />
<glyph unicode="!" horiz-adv-x="462" d="M284 405H173L167 1456H291L284 405ZM153 70Q153 104 175 127T235 151T295 128T318 70Q318 37 296 15T235 -8T175 14T153 70Z" />
<glyph unicode="&quot;" horiz-adv-x="588" d="M243 1396L223 1083H143L146 1536H243V1396ZM479 1396L459 1083H378L382 1536H479V1396Z" />
<glyph unicode="#" horiz-adv-x="1191" d="M753 410H439L362 0H263L340 410H85V503H357L440 944H161V1040H458L537 1456H636L557 1040H872L951 1456H1051L972 1040H1201V944H954L871 503H1126V410H853L776 0H676L753 410ZM456 503H771L854 944H539L456 503Z" />
<glyph unicode="$" d="M901 359Q901 470 829 540T575 674Q349 745 258 842T167 1095Q167 1258 267 1359T539 1475V1677H641V1475Q817 1459 913 1343T1010 1028H891Q891 1185 810 1277T587 1370Q445 1370 366 1296T286 1097Q286 977 359 910T607 783T862 669T981
540T1021 361Q1021 197 919 97T637 -18V-208H536V-19Q335 -6 225 107T115 418H235Q235 262 326 174T580 85Q722 85 811 161T901 359Z" />
<glyph unicode="%" horiz-adv-x="1513" d="M109 1176Q109 1306 189 1391T394 1477T598 1392T679 1170V1099Q679 971 600 886T396 800Q273 800 191 884T109 1106V1176ZM206 1099Q206 1006 257 946T396 886Q481 886 531 946T582 1103V1176Q582 1269 530 1329T394
1390Q311 1390 259 1330T206 1170V1099ZM842 357Q842 487 922 572T1126 657T1330 573T1412 350V279Q1412 149 1332 64T1128 -21T924 63T842 284V357ZM938 279Q938 185 989 125T1128 65Q1214 65 1264 125T1315 284V357Q1315 453 1264 511T1126 570Q1042 570 990
511T938 353V279ZM434 121L359 169L1070 1307L1145 1259L434 121Z" />
<glyph unicode="&amp;" horiz-adv-x="1260" d="M404 794Q317 899 278 981T238 1145Q238 1298 329 1387T573 1476Q712 1476 798 1396T884 1191Q884 1047 718 908L558 784L958 318Q1049 465 1049 651H1160Q1160 403 1032 232L1231 0H1087L961 146Q882 68 779 24T560
-20Q352 -20 230 86T108 371Q108 477 170 571T390 784L404 794ZM560 81Q651 81 736 119T890 229L483 701L469 716L423 681Q227 521 227 371Q227 240 317 161T560 81ZM358 1149Q358 1027 493 861L624 961Q688 1007 729 1062T770 1191Q770 1269 716 1321T572 1374Q474
1374 416 1311T358 1149Z" />
<glyph unicode="&apos;" horiz-adv-x="348" d="M226 1395L209 1090H119Q124 1386 124 1536H226V1395Z" />
<glyph unicode="(" horiz-adv-x="653" d="M140 588Q140 806 196 1011T360 1387T592 1632L621 1551Q555 1504 490 1414T374 1200T292 922T260 571Q260 362 307 169T438 -171T621 -393L592 -470Q465 -394 357 -225T195 148T140 588Z" />
<glyph unicode=")" horiz-adv-x="667" d="M514 573Q514 353 460 150T298 -223T62 -470L33 -393Q131 -323 214 -176T346 166T394 591Q394 798 346 990T214 1334T33 1555L62 1632Q188 1555 295 1386T458 1011T514 573Z" />
<glyph unicode="*" horiz-adv-x="869" d="M361 1000L29 1108L61 1209L393 1086L389 1456H493L485 1083L809 1210L842 1109L509 994L732 700L647 637L433 942L229 639L144 700L361 1000Z" />
<glyph unicode="+" horiz-adv-x="1156" d="M630 740H1073V628H630V146H509V628H75V740H509V1206H630V740Z" />
<glyph unicode="," horiz-adv-x="392" d="M131 -272L60 -220Q151 -98 154 33V188H271V63Q271 -145 131 -272Z" />
<glyph unicode="-" horiz-adv-x="586" d="M528 592H49V693H528V592Z" />
<glyph unicode="." horiz-adv-x="489" d="M145 72Q145 107 167 131T230 156T293 132T316 72T293 15T230 -8T168 14T145 72Z" />
<glyph unicode="/" horiz-adv-x="813" d="M139 -125H30L638 1456H746L139 -125Z" />
<glyph unicode="0" d="M1015 607Q1015 299 902 140T569 -20Q353 -20 238 136T120 592V853Q120 1160 234 1318T567 1476Q783 1476 897 1324T1015 874V607ZM895 868Q895 1118 814 1246T567 1374Q405 1374 323 1249T239 880V594Q239 345 323 213T569 81Q729 81 811
210T895 588V868Z" />
<glyph unicode="1" d="M694 0H574V1312L178 1165V1277L674 1461H694V0Z" />
<glyph unicode="2" d="M1049 0H137V92L636 658Q760 801 808 894T856 1075Q856 1213 775 1293T552 1374Q405 1374 315 1280T224 1036H105Q105 1159 160 1260T318 1418T552 1476Q752 1476 864 1371T977 1085Q977 983 914 862T690 560L284 101H1049V0Z" />
<glyph unicode="3" d="M403 793H527Q630 793 707 829T824 929T865 1076Q865 1216 786 1295T559 1374Q419 1374 330 1292T240 1074H120Q120 1187 177 1280T335 1425T559 1476Q757 1476 871 1368T985 1072Q985 967 919 879T736 746Q872 708 942 616T1012 395Q1012
208 890 94T564 -20Q434 -20 326 32T158 177T98 395H218Q218 256 315 169T564 81Q719 81 805 160T892 391Q892 537 799 614T523 691H403V793Z" />
<glyph unicode="4" d="M872 469H1099V368H872V0H752V368H67V436L741 1456H872V469ZM214 469H752V1301L699 1209L214 469Z" />
<glyph unicode="5" d="M218 746L289 1456H1017V1345H392L341 853Q458 933 615 933Q812 933 929 805T1046 464Q1046 234 932 107T611 -20Q421 -20 303 86T168 383H283Q300 234 384 158T611 81Q767 81 846 180T926 462Q926 622 837 723T594 824Q509 824 446 803T313
719L218 746Z" />
<glyph unicode="6" d="M843 1467V1362H829Q568 1362 418 1209T252 782Q312 865 405 910T613 956Q805 956 918 824T1032 477Q1032 335 979 221T827 44T601 -20Q392 -20 261 131T130 523V643Q130 1034 308 1248T813 1467H843ZM594 853Q480 853 382 786T250 614V512Q250
322 347 202T601 82Q741 82 827 193T914 473Q914 645 828 749T594 853Z" />
<glyph unicode="7" d="M1034 1387L412 0H287L905 1354H77V1456H1034V1387Z" />
<glyph unicode="8" d="M995 1081Q995 968 929 879T755 747Q881 704 957 608T1033 386Q1033 199 906 90T570 -20Q359 -20 233 89T106 386Q106 510 179 607T379 747Q271 789 207 878T143 1081Q143 1262 259 1369T568 1476T877 1368T995 1081ZM913 385Q913 521 816
608T568 696T321 610T225 385T318 164T570 81Q725 81 819 163T913 385ZM875 1082Q875 1207 789 1290T568 1374Q432 1374 348 1294T263 1082Q263 954 347 876T569 798Q704 798 789 876T875 1082Z" />
<glyph unicode="9" d="M884 674Q820 580 725 529T519 477Q395 477 300 541T153 718T101 965Q101 1109 156 1227T311 1410T541 1476Q760 1476 882 1323T1004 887V779Q1004 385 836 187T323 -11H301L302 93H344Q605 97 741 241T884 674ZM534 580Q654 580 749 651T885
837V906Q885 1128 793 1250T543 1373Q401 1373 310 1259T219 970Q219 803 306 692T534 580Z" />
<glyph unicode=":" horiz-adv-x="430" d="M383 72Q383 107 405 131T468 156T531 132T554 72T531 15T468 -8T406 14T383 72ZM129 995Q129 1030 151 1054T214 1079T277 1055T300 995T277 938T214 915T152 937T129 995Z" />
<glyph unicode=";" horiz-adv-x="399" d="M118 995Q118 1030 140 1054T203 1079T266 1055T289 995T266 938T203 915T141 937T118 995ZM131 -272L60 -220Q151 -98 154 33V188H271V63Q271 -145 131 -272Z" />
<glyph unicode="&lt;" horiz-adv-x="1047" d="M208 655L904 355V229L77 608V705L904 1083V957L208 655Z" />
<glyph unicode="=" horiz-adv-x="1133" d="M983 829H149V935H983V829ZM983 418H149V524H983V418Z" />
<glyph unicode="&gt;" horiz-adv-x="1061" d="M835 659L124 962V1085L969 707V610L124 231V355L835 659Z" />
<glyph unicode="?" horiz-adv-x="930" d="M376 404Q378 522 408 594T537 763T664 901T708 990T724 1101Q724 1226 658 1297T472 1369Q352 1369 279 1301T203 1115H84Q86 1279 195 1377T472 1476Q644 1476 743 1376T843 1103Q843 995 794 901T608 680Q495 585 495
404H376ZM360 70Q360 104 381 127T442 151Q480 151 502 128T525 70Q525 37 503 15T442 -8Q403 -8 382 14T360 70Z" />
<glyph unicode="@" horiz-adv-x="1870" d="M1754 513Q1749 366 1700 241T1565 48T1364 -20Q1267 -20 1206 31T1125 174Q1017 -20 827 -20Q687 -20 618 101T567 427Q582 590 641 717T796 916T1001 988Q1078 988 1136 967T1271 880L1220 310Q1210 194 1249 130T1376
66Q1499 66 1575 186T1661 513Q1680 918 1507 1122T983 1327Q772 1327 603 1222T335 923T225 478T291 35T528 -260T906 -363Q998 -363 1087 -341T1236 -284L1267 -364Q1210 -402 1108 -427T902 -453Q652 -453 472 -341T203 -17T125 478Q137 756 247 970T550 1302T987
1420Q1242 1420 1419 1314T1681 1002T1754 513ZM673 286Q684 186 729 132T848 77Q1033 77 1121 332L1166 848Q1099 897 1008 897Q897 897 816 809T696 565T673 286Z" />
<glyph unicode="A" horiz-adv-x="1279" d="M970 408H309L159 0H30L581 1456H698L1249 0H1121L970 408ZM347 513H931L639 1306L347 513Z" />
<glyph unicode="B" horiz-adv-x="1255" d="M184 0V1456H614Q848 1456 969 1360T1090 1075Q1090 962 1029 879T860 759Q987 731 1064 634T1142 410Q1142 217 1018 109T671 0H184ZM307 700V104H676Q834 104 926 184T1019 408Q1019 543 931 621T686 700H307ZM307
803H643Q797 806 881 875T966 1078Q966 1218 879 1284T614 1351H307V803Z" />
<glyph unicode="C" horiz-adv-x="1330" d="M1215 454Q1190 224 1051 102T679 -20Q517 -20 393 61T200 290T131 630V819Q131 1013 199 1163T394 1394T688 1476Q922 1476 1057 1350T1215 1000H1091Q1045 1371 688 1371Q490 1371 373 1223T255 814V636Q255 384 369
234T679 84Q872 84 970 176T1091 454H1215Z" />
<glyph unicode="D" horiz-adv-x="1341" d="M184 0V1456H591Q770 1456 912 1375T1133 1141T1213 795V661Q1213 466 1134 315T912 82T582 0H184ZM307 1351V104H583Q813 104 952 256T1091 669V797Q1091 1048 954 1199T593 1351H307Z" />
<glyph unicode="E" horiz-adv-x="1165" d="M988 698H307V104H1090V0H184V1456H1085V1351H307V802H988V698Z" />
<glyph unicode="F" horiz-adv-x="1152" d="M986 680H307V0H184V1456H1086V1351H307V785H986V680Z" />
<glyph unicode="G" horiz-adv-x="1400" d="M1235 173Q1171 82 1035 31T729 -20Q558 -20 425 62T219 294T145 638V822Q145 1125 298 1300T709 1476Q934 1476 1071 1362T1234 1046H1111Q1084 1206 981 1288T710 1371Q506 1371 387 1226T268 817V645Q268 479 324
352T486 154T729 84Q888 84 1002 134Q1076 167 1112 211V587H721V691H1235V173Z" />
<glyph unicode="H" horiz-adv-x="1449" d="M1263 0H1139V698H307V0H184V1456H307V802H1139V1456H1263V0Z" />
<glyph unicode="I" horiz-adv-x="545" d="M334 0H211V1456H334V0Z" />
<glyph unicode="J" horiz-adv-x="1127" d="M827 1456H951V433Q951 226 832 103T511 -20Q299 -20 185 91T71 401H194Q194 243 277 164T511 84Q650 84 737 176T827 426V1456Z" />
<glyph unicode="K" horiz-adv-x="1292" d="M512 723L307 521V0H184V1456H307V671L1053 1456H1208L598 808L1255 0H1105L512 723Z" />
<glyph unicode="L" horiz-adv-x="1079" d="M308 104H1027V0H184V1456H308V104Z" />
<glyph unicode="M" horiz-adv-x="1772" d="M347 1456L884 171L1423 1456H1587V0H1464V634L1474 1284L932 0H837L297 1279L307 638V0H184V1456H347Z" />
<glyph unicode="N" horiz-adv-x="1454" d="M1268 0H1145L308 1246V0H184V1456H308L1146 209V1456H1268V0Z" />
<glyph unicode="O" horiz-adv-x="1386" d="M1260 649Q1260 448 1191 296T992 62T694 -20Q439 -20 282 162T125 655V805Q125 1004 195 1157T395 1393T692 1476T988 1395T1187 1166T1260 823V649ZM1137 807Q1137 1070 1018 1219T692 1368Q489 1368 369 1219T248
801V649Q248 390 368 239T694 87Q903 87 1020 236T1137 653V807Z" />
<glyph unicode="P" horiz-adv-x="1261" d="M307 593V0H184V1456H680Q907 1456 1038 1340T1170 1021Q1170 816 1044 705T677 593H307ZM307 697H680Q859 697 953 782T1047 1019Q1047 1170 954 1259T688 1351H307V697Z" />
<glyph unicode="Q" horiz-adv-x="1386" d="M1256 649Q1256 441 1183 287T973 53L1238 -178L1153 -254L856 3Q774 -20 689 -20Q523 -20 394 62T193 294T121 642V805Q121 1004 191 1157T391 1393T687 1476Q857 1476 986 1394T1185 1159T1256 806V649ZM1133 807Q1133
1070 1014 1219T687 1368Q485 1368 365 1219T244 801V649Q244 390 363 239T689 87Q897 87 1015 236T1133 652V807Z" />
<glyph unicode="R" horiz-adv-x="1300" d="M728 606H305V0H181V1456H654Q887 1456 1018 1343T1149 1027Q1149 887 1067 780T847 632L1211 13V0H1080L728 606ZM305 711H682Q837 711 931 799T1025 1027Q1025 1181 927 1266T652 1351H305V711Z" />
<glyph unicode="S" horiz-adv-x="1213" d="M1008 358Q1008 479 923 549T612 683T282 822Q134 928 134 1100Q134 1267 271 1371T623 1476Q768 1476 882 1420T1060 1264T1123 1041H999Q999 1190 897 1280T623 1371Q456 1371 357 1297T258 1102Q258 991 347 921T632
798T929 687T1081 549T1132 360Q1132 188 995 84T632 -20Q478 -20 350 35T155 189T88 416H211Q211 262 326 173T632 84Q802 84 905 159T1008 358Z" />
<glyph unicode="T" horiz-adv-x="1223" d="M1172 1351H673V0H550V1351H52V1456H1172V1351Z" />
<glyph unicode="U" horiz-adv-x="1346" d="M1187 1456V462Q1186 315 1122 206T942 39T674 -20Q444 -20 306 105T162 453V1456H284V471Q284 287 389 186T674 84T958 186T1063 470V1456H1187Z" />
<glyph unicode="V" horiz-adv-x="1263" d="M623 180L631 149L640 180L1098 1456H1233L691 0H573L31 1456H165L623 180Z" />
<glyph unicode="W" horiz-adv-x="1836" d="M453 393L498 167L553 383L869 1456H980L1292 383L1346 165L1394 393L1657 1456H1783L1410 0H1292L962 1139L925 1283L889 1139L551 0H433L61 1456H187L453 393Z" />
<glyph unicode="X" horiz-adv-x="1253" d="M627 840L1037 1456H1184L702 738L1199 0H1051L627 636L201 0H55L553 738L70 1456H217L627 840Z" />
<glyph unicode="Y" horiz-adv-x="1226" d="M611 662L1056 1456H1198L672 548V0H549V548L24 1456H170L611 662Z" />
<glyph unicode="Z" horiz-adv-x="1225" d="M239 104H1138V0H90V93L954 1351H116V1456H1106V1368L239 104Z" />
<glyph unicode="[" horiz-adv-x="491" d="M493 1562H283V-210H493V-312H163V1664H493V1562Z" />
<glyph unicode="\" horiz-adv-x="807" d="M48 1456H165L773 -125H656L48 1456Z" />
<glyph unicode="]" horiz-adv-x="491" d="M0 1664H331V-312H0V-210H211V1562H0V1664Z" />
<glyph unicode="^" horiz-adv-x="852" d="M421 1298L193 729H77L376 1456H466L764 729H648L421 1298Z" />
<glyph unicode="_" horiz-adv-x="884" d="M882 -101H1V0H882V-101Z" />
<glyph unicode="`" horiz-adv-x="585" d="M438 1256H329L103 1536H247L438 1256Z" />
<glyph unicode="a" horiz-adv-x="1097" d="M839 0Q821 51 816 151Q753 69 656 25T449 -20Q293 -20 197 67T100 287Q100 445 231 537T598 629H815V752Q815 868 744 934T535 1001Q410 1001 328 937T246 783L126 784Q126 913 246 1007T541 1102Q722 1102 826 1012T934
759V247Q934 90 967 12V0H839ZM463 86Q583 86 677 144T815 299V537H601Q422 535 321 472T220 297Q220 206 287 146T463 86Z" />
<glyph unicode="b" d="M1027 530Q1027 277 915 129T614 -20Q388 -20 272 148L267 0H155V1536H274V925Q388 1102 612 1102Q804 1102 915 956T1027 548V530ZM907 551Q907 765 824 881T590 998Q475 998 395 942T274 776V288Q364 84 592 84Q740 84 823 201T907 551Z" />
<glyph unicode="c" horiz-adv-x="1055" d="M556 81Q681 81 765 151T857 334H972Q967 235 910 154T759 26T556 -20Q343 -20 219 128T94 526V562Q94 722 150 845T310 1035T555 1102Q733 1102 848 996T972 717H857Q849 844 766 922T555 1000Q393 1000 304 883T214
555V520Q214 313 303 197T556 81Z" />
<glyph unicode="d" horiz-adv-x="1138" d="M108 551Q108 803 220 952T526 1102Q745 1102 860 929V1536H979V0H867L862 144Q747 -20 524 -20Q337 -20 223 130T108 537V551ZM229 530Q229 323 312 204T546 84Q767 84 860 279V787Q767 998 548 998Q397 998 313 880T229 530Z" />
<glyph unicode="e" horiz-adv-x="1058" d="M575 -20Q437 -20 326 48T152 237T90 510V553Q90 709 150 834T319 1030T553 1102Q750 1102 865 968T981 600V533H209V510Q209 326 314 204T580 81Q676 81 749 116T883 228L958 171Q826 -20 575 -20ZM553 1000Q418 1000
326 901T213 635H862V648Q857 804 773 902T553 1000Z" />
<glyph unicode="f" horiz-adv-x="678" d="M242 0V984H63V1082H242V1213Q242 1379 326 1468T562 1557Q630 1557 689 1540L680 1440Q630 1452 571 1452Q472 1452 417 1391T362 1216V1082H620V984H362V0H242Z" />
<glyph unicode="g" horiz-adv-x="1136" d="M108 551Q108 805 220 953T526 1102Q747 1102 862 926L868 1082H980V22Q980 -187 863 -309T546 -431Q433 -431 331 -381T169 -246L236 -174Q363 -330 538 -330Q688 -330 772 -242T859 4V140Q744 -20 524 -20Q336 -20
222 130T108 535V551ZM229 530Q229 323 312 204T546 84Q767 84 859 282V785Q817 889 738 943T548 998Q397 998 313 880T229 530Z" />
<glyph unicode="h" horiz-adv-x="1124" d="M275 899Q334 996 426 1049T627 1102Q801 1102 886 1004T972 710V0H853V711Q852 856 792 927T598 998Q487 998 402 929T275 741V0H156V1536H275V899Z" />
<glyph unicode="i" horiz-adv-x="459" d="M290 0H170V1082H290V0ZM149 1395Q149 1429 171 1452T231 1476T291 1453T314 1395T292 1338T231 1315T171 1338T149 1395Z" />
<glyph unicode="j" horiz-adv-x="467" d="M285 1082V-129Q285 -279 213 -358T1 -437Q-53 -437 -104 -418L-102 -319Q-58 -332 -12 -332Q166 -332 166 -127V1082H285ZM226 1476Q265 1476 287 1453T309 1395T287 1338T226 1315Q188 1315 167 1338T145 1395T166 1452T226
1476Z" />
<glyph unicode="k" horiz-adv-x="1003" d="M413 545L276 413V0H156V1536H276V553L389 675L803 1082H954L495 626L994 0H851L413 545Z" />
<glyph unicode="l" horiz-adv-x="459" d="M290 0H170V1536H290V0Z" />
<glyph unicode="m" horiz-adv-x="1815" d="M265 1082L269 906Q329 1004 419 1053T619 1102Q875 1102 944 892Q1002 993 1099 1047T1313 1102Q1661 1102 1668 722V0H1548V713Q1547 858 1486 928T1285 998Q1156 996 1067 915T968 716V0H848V722Q847 861 783 929T584
998Q471 998 390 934T270 742V0H150V1082H265Z" />
<glyph unicode="n" horiz-adv-x="1125" d="M270 1082L274 897Q335 997 426 1049T627 1102Q801 1102 886 1004T972 710V0H853V711Q852 856 792 927T598 998Q487 998 402 929T275 741V0H156V1082H270Z" />
<glyph unicode="o" horiz-adv-x="1147" d="M90 557Q90 713 150 838T321 1032T572 1102Q788 1102 922 951T1056 549V524Q1056 367 996 242T825 48T574 -20Q359 -20 225 131T90 533V557ZM210 524Q210 330 310 206T574 81Q736 81 836 205T937 534V557Q937 681 891
784T762 943T572 1000Q412 1000 311 875T210 546V524Z" />
<glyph unicode="p" d="M1026 530Q1026 277 914 129T614 -20Q392 -20 274 136V-416H155V1082H266L272 929Q389 1102 611 1102Q805 1102 915 955T1026 547V530ZM906 551Q906 758 821 878T584 998Q474 998 395 945T274 791V272Q317 179 397 130T586 81Q737 81 821
201T906 551Z" />
<glyph unicode="q" horiz-adv-x="1142" d="M108 551Q108 805 220 953T528 1102Q747 1102 861 935L867 1082H979V-416H859V134Q741 -20 526 -20Q336 -20 222 130T108 535V551ZM229 530Q229 320 313 201T548 81Q763 81 859 268V798Q814 895 735 947T550 1000Q399
1000 314 881T229 530Z" />
<glyph unicode="r" horiz-adv-x="689" d="M656 980Q618 987 575 987Q463 987 386 925T275 743V0H156V1082H273L275 910Q370 1102 580 1102Q630 1102 659 1089L656 980Z" />
<glyph unicode="s" horiz-adv-x="1037" d="M804 275Q804 364 733 418T517 502T294 572T176 669T137 807Q137 935 244 1018T518 1102Q699 1102 808 1013T918 779H798Q798 874 719 937T518 1000Q400 1000 329 948T257 811Q257 730 316 686T533 604T769 525T886 424T924
281Q924 144 814 62T525 -20Q336 -20 219 71T101 303H221Q228 198 309 140T525 81Q650 81 727 136T804 275Z" />
<glyph unicode="t" horiz-adv-x="658" d="M342 1359V1082H566V984H342V263Q342 173 374 129T483 85Q513 85 580 95L585 -3Q538 -20 457 -20Q334 -20 278 51T222 262V984H23V1082H222V1359H342Z" />
<glyph unicode="u" horiz-adv-x="1125" d="M852 137Q744 -20 507 -20Q334 -20 244 80T152 378V1082H271V393Q271 84 521 84Q781 84 850 299V1082H970V0H854L852 137Z" />
<glyph unicode="v" horiz-adv-x="985" d="M493 165L822 1082H945L541 0H444L38 1082H161L493 165Z" />
<glyph unicode="w" horiz-adv-x="1544" d="M415 249L433 156L457 254L717 1082H819L1076 261L1104 147L1127 252L1349 1082H1473L1158 0H1056L778 858L765 917L752 857L479 0H377L63 1082H186L415 249Z" />
<glyph unicode="x" horiz-adv-x="996" d="M496 643L788 1082H930L563 551L946 0H805L497 458L189 0H48L430 551L63 1082H204L496 643Z" />
<glyph unicode="y" horiz-adv-x="973" d="M499 172L815 1082H944L482 -184L458 -240Q369 -437 183 -437Q140 -437 91 -423L90 -324L152 -330Q240 -330 294 -287T387 -137L440 9L32 1082H163L499 172Z" />
<glyph unicode="z" horiz-adv-x="996" d="M235 101H938V0H87V88L743 979H107V1082H894V993L235 101Z" />
<glyph unicode="{" horiz-adv-x="676" d="M637 -404Q469 -354 384 -241T299 59V280Q299 543 68 543V647Q299 647 299 908V1137Q300 1320 384 1433T637 1597L663 1518Q419 1440 419 1127V914Q419 668 235 595Q419 518 419 277V49Q423 -243 666 -324L637 -404Z" />
<glyph unicode="|" horiz-adv-x="452" d="M279 -270H178V1456H279V-270Z" />
<glyph unicode="}" horiz-adv-x="676" d="M9 -324Q252 -243 256 49V273Q256 526 449 594Q256 662 256 913V1126Q256 1442 12 1518L38 1597Q209 1546 292 1432T376 1131V908Q376 647 607 647V543Q376 543 376 280V59Q376 -128 291 -241T38 -404L9 -324Z" />
<glyph unicode="~" horiz-adv-x="1402" d="M1254 764Q1254 615 1171 519T958 423Q886 423 824 450T670 558T535 659T441 680Q352 680 303 621T253 450L145 449Q145 598 226 692T441 787Q515 787 581 756T740 643Q807 580 855 555T958 529Q1046 529 1098 592T1150
764H1254Z" />
<glyph unicode="&#xa0;" horiz-adv-x="498" />
<glyph unicode="&#xa1;" horiz-adv-x="452" d="M174 690H285L292 -359H168L174 690ZM305 1022Q305 988 283 965T223 942T163 965T140 1022T162 1079T223 1102T283 1079T305 1022Z" />
<glyph unicode="&#xa2;" horiz-adv-x="1115" d="M581 81Q704 81 788 150T882 334H997Q989 195 887 97T636 -17V-245H516V-16Q331 7 225 150T119 526V562Q119 784 224 929T516 1098V1318H636V1099Q791 1083 891 978T997 717H882Q874 844 791 922T580 1000Q418 1000
329 883T239 555V520Q239 313 328 197T581 81Z" />
<glyph unicode="&#xa3;" horiz-adv-x="1170" d="M404 645L413 368Q415 194 349 104H1094V0H97V104H195Q246 117 272 211Q292 285 290 367L281 645H93V749H277L268 1039Q268 1239 378 1357T674 1476Q856 1476 961 1371T1067 1088H944Q944 1223 869 1297T665 1371Q540
1371 466 1283T392 1039L401 749H745V645H404Z" />
<glyph unicode="&#xa4;" horiz-adv-x="1481" d="M1131 133Q1053 61 953 21T740 -20Q514 -20 349 132L194 -26L109 60L268 221Q144 389 144 608Q144 835 277 1006L109 1177L194 1264L361 1094Q526 1234 740 1234T1119 1092L1289 1265L1375 1177L1204 1002Q1334
832 1334 608Q1334 393 1212 224L1375 60L1289 -27L1131 133ZM257 608Q257 470 321 350T499 161T740 91Q869 91 981 161T1157 350T1221 608Q1221 747 1156 866T979 1054T740 1122T500 1054T323 867T257 608Z" />
<glyph unicode="&#xa5;" horiz-adv-x="1056" d="M527 731L892 1456H1030L631 705H944V616H586V412H944V324H586V0H463V324H109V412H463V616H109V705H422L24 1456H163L527 731Z" />
<glyph unicode="&#xa6;" horiz-adv-x="444" d="M159 -270V501H279V-270H159ZM279 698H159V1456H279V698Z" />
<glyph unicode="&#xa7;" horiz-adv-x="1239" d="M1119 431Q1119 331 1058 262T887 159Q978 111 1026 41T1075 -139Q1075 -303 949 -399T606 -495Q497 -495 401 -467T236 -382Q102 -268 102 -64L222 -62Q222 -218 325 -305T606 -393Q766 -393 860 -324T954 -141Q954
-64 920 -17T805 69T548 156T284 255T153 378T108 551Q108 651 166 721T331 825Q245 872 199 942T153 1120Q153 1281 282 1378T624 1476Q848 1476 972 1363T1097 1045H977Q977 1191 881 1282T624 1374Q459 1374 366 1306T273 1122Q273 1043 304 996T411 911T646
828Q842 777 936 726T1075 603T1119 431ZM454 771Q346 758 287 700T228 553Q228 470 263 422T379 336T663 242L755 214Q867 227 933 284T999 428Q999 526 932 585T692 700L454 771Z" />
<glyph unicode="&#xa8;" horiz-adv-x="881" d="M137 1396Q137 1430 159 1453T219 1477T279 1454T302 1396Q302 1363 280 1340T219 1317T159 1340T137 1396ZM575 1395Q575 1429 597 1452T657 1476T717 1453T740 1395Q740 1362 718 1339T657 1316T597 1339T575 1395Z" />
<glyph unicode="&#xa9;" horiz-adv-x="1637" d="M1121 607Q1121 455 1039 374T807 293T566 399T474 686V776Q474 950 566 1056T807 1163T1039 1083T1122 850H1023Q1023 1074 807 1074Q701 1074 637 993T573 771V680Q573 546 636 465T807 383Q913 383 967 436T1022
607H1121ZM192 729Q192 553 273 399T502 155T817 65Q984 65 1129 154T1357 396T1441 729Q1441 907 1358 1059T1130 1300T817 1389Q646 1389 499 1298T272 1055T192 729ZM107 729Q107 931 200 1104T459 1376T817 1476T1174 1377T1432 1104T1526 729Q1526 532 1436
360T1181 84T817 -21Q620 -21 455 82T198 358T107 729Z" />
<glyph unicode="&#xaa;" horiz-adv-x="906" d="M649 705Q634 748 628 799Q541 691 406 691Q289 691 223 749T157 908Q157 1018 240 1079T486 1140H625V1201Q625 1286 585 1333T464 1380Q374 1380 323 1345T271 1237L164 1243Q164 1345 247 1410T464 1476Q588 1476
661 1405T734 1199V884Q734 792 760 705H649ZM426 786Q479 786 536 816T625 890V1058H496Q266 1058 266 912Q266 786 426 786Z" />
<glyph unicode="&#xab;" horiz-adv-x="933" d="M247 792L523 404H418L123 783V802L418 1181H523L247 792ZM556 536L832 148H727L432 527V546L727 925H832L556 536Z" />
<glyph unicode="&#xac;" horiz-adv-x="1117" d="M936 386H816V670H124V776H936V386Z" />
<glyph unicode="&#xad;" horiz-adv-x="586" d="M528 592H49V693H528V592Z" />
<glyph unicode="&#xae;" horiz-adv-x="1642" d="M102 729Q102 931 195 1104T454 1376T812 1476T1169 1377T1428 1104T1522 729Q1522 530 1431 358T1175 83T812 -21T450 82T193 358T102 729ZM187 729Q187 550 270 396T499 154T812 65T1125 153T1353 396T1436 729Q1436
905 1355 1057T1129 1299T812 1389Q644 1389 499 1301T270 1060T187 729ZM650 666V321H552V1160H810Q957 1160 1036 1099T1115 912Q1115 779 974 715Q1046 689 1074 635T1102 504T1106 394T1119 337V321H1017Q1003 357 1003 503Q1003 592 966 629T838 666H650ZM650
757H831Q912 757 964 799T1017 910Q1017 995 974 1031T824 1070H650V757Z" />
<glyph unicode="&#xaf;" horiz-adv-x="874" d="M756 1343H137V1440H756V1343Z" />
<glyph unicode="&#xb0;" horiz-adv-x="774" d="M630 1226Q630 1122 559 1051T388 980Q287 980 215 1051T143 1226T216 1402T388 1476T558 1403T630 1226ZM233 1226Q233 1159 277 1115T388 1071T497 1115T540 1226Q540 1295 497 1340T388 1385Q323 1385 278 1340T233
1226Z" />
<glyph unicode="&#xb1;" horiz-adv-x="1085" d="M609 829H1000V727H609V289H498V727H84V829H498V1267H609V829ZM963 0H128V101H963V0Z" />
<glyph unicode="&#xb2;" horiz-adv-x="740" d="M667 665H96V740L416 1054Q522 1164 522 1237Q522 1300 482 1338T362 1377Q275 1377 228 1333T181 1215H76Q76 1323 155 1394T360 1465T557 1403T628 1239Q628 1138 510 1016L455 961L229 752H667V665Z" />
<glyph unicode="&#xb3;" horiz-adv-x="740" d="M267 1107H353Q434 1109 481 1145T529 1241Q529 1303 486 1340T362 1377Q286 1377 238 1340T190 1245H85Q85 1341 163 1403T361 1465Q489 1465 562 1405T635 1243Q635 1187 597 1140T489 1069Q651 1027 651 880Q651
778 572 716T363 654Q234 654 153 717T71 884H177Q177 822 229 782T366 741Q453 741 499 779T546 883Q546 1025 340 1025H267V1107Z" />
<glyph unicode="&#xb4;" horiz-adv-x="576" d="M315 1536H460L229 1256H124L315 1536Z" />
<glyph unicode="&#xb5;" horiz-adv-x="1140" d="M281 1082V446Q281 266 344 174T544 81Q676 81 753 138T859 312V1082H979V0H870L863 154Q765 -20 552 -20Q368 -20 281 105V-416H162V1082H281Z" />
<glyph unicode="&#xb6;" horiz-adv-x="973" d="M681 0V520H573Q423 520 312 578T142 742T83 988Q83 1201 216 1328T577 1456H801V0H681Z" />
<glyph unicode="&#xb7;" horiz-adv-x="503" d="M163 717Q163 752 185 776T247 800T310 776T333 717T310 659T247 635T185 658T163 717Z" />
<glyph unicode="&#xb8;" horiz-adv-x="498" d="M246 0L234 -64Q399 -85 399 -235Q399 -327 320 -381T105 -435L98 -357Q187 -357 243 -325T300 -237Q300 -179 257 -157T124 -127L153 0H246Z" />
<glyph unicode="&#xb9;" horiz-adv-x="740" d="M464 665H358V1328L126 1258V1348L450 1455H464V665Z" />
<glyph unicode="&#xba;" horiz-adv-x="922" d="M135 1132Q135 1285 223 1380T458 1476Q605 1476 693 1381T782 1127V1033Q782 880 694 785T460 690Q313 690 224 784T135 1038V1132ZM243 1033Q243 919 299 852T460 785Q559 785 616 851T674 1037V1132Q674 1247
616 1313T458 1380T301 1312T243 1127V1033Z" />
<glyph unicode="&#xbb;" horiz-adv-x="928" d="M221 944L516 560V541L221 162H115L391 550L115 944H221ZM540 944L835 560V541L540 162H434L710 550L434 944H540Z" />
<glyph unicode="&#xbc;" horiz-adv-x="1484" d="M453 664H347V1327L115 1257V1347L439 1454H453V664ZM414 129L340 177L1051 1315L1125 1267L414 129ZM1272 275H1399V187H1272V0H1167V187H768L764 253L1161 789H1272V275ZM878 275H1167V659L1136 609L878 275Z" />
<glyph unicode="&#xbd;" horiz-adv-x="1548" d="M370 129L296 177L1007 1315L1081 1267L370 129ZM438 664H332V1327L100 1257V1347L424 1454H438V664ZM1436 0H865V75L1185 389Q1291 499 1291 572Q1291 635 1251 673T1131 712Q1044 712 997 668T950 550H845Q845
658 924 729T1129 800T1326 738T1397 574Q1397 473 1279 351L1224 296L998 87H1436V0Z" />
<glyph unicode="&#xbe;" horiz-adv-x="1590" d="M558 129L484 177L1195 1315L1269 1267L558 129ZM1387 275H1514V187H1387V0H1282V187H883L879 253L1276 789H1387V275ZM993 275H1282V659L1251 609L993 275ZM314 1107H400Q481 1109 528 1145T576 1241Q576 1303
533 1340T409 1377Q333 1377 285 1340T237 1245H132Q132 1341 210 1403T408 1465Q536 1465 609 1405T682 1243Q682 1187 644 1140T536 1069Q698 1027 698 880Q698 778 619 716T410 654Q281 654 200 717T118 884H224Q224 822 276 782T413 741Q500 741 546 779T593
883Q593 1025 387 1025H314V1107Z" />
<glyph unicode="&#xbf;" horiz-adv-x="940" d="M551 687Q549 564 524 505T405 352T288 228Q207 123 207 -8Q207 -137 274 -207T469 -277Q588 -277 659 -207T732 -20H852Q850 -186 745 -284T469 -383Q291 -383 190 -283T88 -10Q88 101 141 202T337 438Q422 509
429 618L431 687H551ZM567 1022Q567 988 545 965T485 941T425 964T402 1022Q402 1055 424 1078T485 1101T545 1078T567 1022Z" />
<glyph unicode="&#xc0;" horiz-adv-x="1279" d="M970 408H309L159 0H30L581 1456H698L1249 0H1121L970 408ZM347 513H931L639 1306L347 513ZM716 1571H607L381 1851H525L716 1571Z" />
<glyph unicode="&#xc1;" horiz-adv-x="1279" d="M970 408H309L159 0H30L581 1456H698L1249 0H1121L970 408ZM347 513H931L639 1306L347 513ZM762 1851H907L676 1571H571L762 1851Z" />
<glyph unicode="&#xc2;" horiz-adv-x="1279" d="M970 408H309L159 0H30L581 1456H698L1249 0H1121L970 408ZM347 513H931L639 1306L347 513ZM921 1583V1573H810L642 1756L475 1573H366V1586L604 1841H680L921 1583Z" />
<glyph unicode="&#xc3;" horiz-adv-x="1279" d="M970 408H309L159 0H30L581 1456H698L1249 0H1121L970 408ZM347 513H931L639 1306L347 513ZM983 1809Q983 1713 927 1655T788 1596Q712 1596 640 1651T510 1706Q463 1706 432 1675T400 1588L310 1591Q310 1683 364
1743T505 1803Q553 1803 587 1786T651 1748T711 1710T783 1693Q829 1693 861 1726T894 1815L983 1809Z" />
<glyph unicode="&#xc4;" horiz-adv-x="1279" d="M970 408H309L159 0H30L581 1456H698L1249 0H1121L970 408ZM347 513H931L639 1306L347 513ZM343 1711Q343 1745 365 1768T425 1792T485 1769T508 1711Q508 1678 486 1655T425 1632T365 1655T343 1711ZM781 1710Q781
1744 803 1767T863 1791T923 1768T946 1710Q946 1677 924 1654T863 1631T803 1654T781 1710Z" />
<glyph unicode="&#xc5;" horiz-adv-x="1279" d="M970 408H309L159 0H30L581 1456H698L1249 0H1121L970 408ZM347 513H931L639 1306L347 513ZM450 1715Q450 1795 506 1850T643 1905Q722 1905 779 1850T836 1715Q836 1636 781 1582T643 1528T505 1582T450 1715ZM527
1715Q527 1665 560 1632T643 1599Q692 1599 726 1631T760 1715Q760 1768 725 1801T643 1834Q594 1834 561 1800T527 1715Z" />
<glyph unicode="&#xc6;" horiz-adv-x="1865" d="M1823 0H1006L989 389H393L163 0H17L898 1456H1762V1354H1068L1091 809H1680V707H1095L1121 101H1823V0ZM460 502H985L950 1331L460 502Z" />
<glyph unicode="&#xc7;" horiz-adv-x="1330" d="M1215 454Q1190 224 1051 102T679 -20Q517 -20 393 61T200 290T131 630V819Q131 1013 199 1163T394 1394T688 1476Q922 1476 1057 1350T1215 1000H1091Q1045 1371 688 1371Q490 1371 373 1223T255 814V636Q255 384
369 234T679 84Q872 84 970 176T1091 454H1215ZM728 -9L716 -73Q881 -94 881 -244Q881 -336 802 -390T587 -444L580 -366Q669 -366 725 -334T782 -246Q782 -188 739 -166T606 -136L635 -9H728Z" />
<glyph unicode="&#xc8;" horiz-adv-x="1165" d="M988 698H307V104H1090V0H184V1456H1085V1351H307V802H988V698ZM693 1577H584L358 1857H502L693 1577Z" />
<glyph unicode="&#xc9;" horiz-adv-x="1165" d="M988 698H307V104H1090V0H184V1456H1085V1351H307V802H988V698ZM739 1857H884L653 1577H548L739 1857Z" />
<glyph unicode="&#xca;" horiz-adv-x="1165" d="M988 698H307V104H1090V0H184V1456H1085V1351H307V802H988V698ZM898 1589V1579H787L619 1762L452 1579H343V1592L581 1847H657L898 1589Z" />
<glyph unicode="&#xcb;" horiz-adv-x="1165" d="M988 698H307V104H1090V0H184V1456H1085V1351H307V802H988V698ZM320 1717Q320 1751 342 1774T402 1798T462 1775T485 1717Q485 1684 463 1661T402 1638T342 1661T320 1717ZM758 1716Q758 1750 780 1773T840 1797T900
1774T923 1716Q923 1683 901 1660T840 1637T780 1660T758 1716Z" />
<glyph unicode="&#xcc;" horiz-adv-x="545" d="M334 0H211V1456H334V0ZM348 1577H239L13 1857H157L348 1577Z" />
<glyph unicode="&#xcd;" horiz-adv-x="545" d="M334 0H211V1456H334V0ZM393 1857H538L307 1577H202L393 1857Z" />
<glyph unicode="&#xce;" horiz-adv-x="545" d="M334 0H211V1456H334V0ZM553 1589V1579H442L274 1762L107 1579H-2V1592L236 1847H312L553 1589Z" />
<glyph unicode="&#xcf;" horiz-adv-x="545" d="M334 0H211V1456H334V0ZM-25 1717Q-25 1751 -3 1774T57 1798T117 1775T140 1717Q140 1684 118 1661T57 1638T-3 1661T-25 1717ZM413 1716Q413 1750 435 1773T495 1797T555 1774T578 1716Q578 1683 556 1660T495 1637T435
1660T413 1716Z" />
<glyph unicode="&#xd0;" horiz-adv-x="1371" d="M214 0V689H33V791H214V1456H621Q800 1456 942 1375T1163 1141T1243 795V661Q1243 466 1164 315T942 82T612 0H214ZM645 689H337V104H608Q843 104 982 256T1121 669V797Q1121 1048 984 1199T623 1351H337V791H645V689Z" />
<glyph unicode="&#xd1;" horiz-adv-x="1454" d="M1268 0H1145L308 1246V0H184V1456H308L1146 209V1456H1268V0ZM1067 1809Q1067 1713 1011 1655T872 1596Q796 1596 724 1651T594 1706Q547 1706 516 1675T484 1588L394 1591Q394 1683 448 1743T589 1803Q637 1803
671 1786T735 1748T795 1710T867 1693Q913 1693 945 1726T978 1815L1067 1809Z" />
<glyph unicode="&#xd2;" horiz-adv-x="1386" d="M1260 649Q1260 448 1191 296T992 62T694 -20Q439 -20 282 162T125 655V805Q125 1004 195 1157T395 1393T692 1476T988 1395T1187 1166T1260 823V649ZM1137 807Q1137 1070 1018 1219T692 1368Q489 1368 369 1219T248
801V649Q248 390 368 239T694 87Q903 87 1020 236T1137 653V807ZM765 1583H656L430 1863H574L765 1583Z" />
<glyph unicode="&#xd3;" horiz-adv-x="1386" d="M1260 649Q1260 448 1191 296T992 62T694 -20Q439 -20 282 162T125 655V805Q125 1004 195 1157T395 1393T692 1476T988 1395T1187 1166T1260 823V649ZM1137 807Q1137 1070 1018 1219T692 1368Q489 1368 369 1219T248
801V649Q248 390 368 239T694 87Q903 87 1020 236T1137 653V807ZM811 1863H956L725 1583H620L811 1863Z" />
<glyph unicode="&#xd4;" horiz-adv-x="1386" d="M1260 649Q1260 448 1191 296T992 62T694 -20Q439 -20 282 162T125 655V805Q125 1004 195 1157T395 1393T692 1476T988 1395T1187 1166T1260 823V649ZM1137 807Q1137 1070 1018 1219T692 1368Q489 1368 369 1219T248
801V649Q248 390 368 239T694 87Q903 87 1020 236T1137 653V807ZM970 1595V1585H859L691 1768L524 1585H415V1598L653 1853H729L970 1595Z" />
<glyph unicode="&#xd5;" horiz-adv-x="1386" d="M1260 649Q1260 448 1191 296T992 62T694 -20Q439 -20 282 162T125 655V805Q125 1004 195 1157T395 1393T692 1476T988 1395T1187 1166T1260 823V649ZM1137 807Q1137 1070 1018 1219T692 1368Q489 1368 369 1219T248
801V649Q248 390 368 239T694 87Q903 87 1020 236T1137 653V807ZM1032 1821Q1032 1725 976 1667T837 1608Q761 1608 689 1663T559 1718Q512 1718 481 1687T449 1600L359 1603Q359 1695 413 1755T554 1815Q602 1815 636 1798T700 1760T760 1722T832 1705Q878 1705
910 1738T943 1827L1032 1821Z" />
<glyph unicode="&#xd6;" horiz-adv-x="1386" d="M1260 649Q1260 448 1191 296T992 62T694 -20Q439 -20 282 162T125 655V805Q125 1004 195 1157T395 1393T692 1476T988 1395T1187 1166T1260 823V649ZM1137 807Q1137 1070 1018 1219T692 1368Q489 1368 369 1219T248
801V649Q248 390 368 239T694 87Q903 87 1020 236T1137 653V807ZM392 1723Q392 1757 414 1780T474 1804T534 1781T557 1723Q557 1690 535 1667T474 1644T414 1667T392 1723ZM830 1722Q830 1756 852 1779T912 1803T972 1780T995 1722Q995 1689 973 1666T912 1643T852
1666T830 1722Z" />
<glyph unicode="&#xd7;" horiz-adv-x="1072" d="M93 179L451 544L108 894L187 974L529 624L872 974L951 894L608 544L966 179L887 100L529 464L172 100L93 179Z" />
<glyph unicode="&#xd8;" horiz-adv-x="1386" d="M1260 649Q1260 448 1191 296T992 62T694 -20Q508 -20 375 77L274 -83H170L307 134Q125 318 125 658V805Q125 1004 195 1157T395 1393T692 1476Q916 1476 1064 1336L1171 1505H1274L1125 1268Q1259 1088 1260 807V649ZM248
649Q248 388 370 235L1002 1237Q883 1368 692 1368Q489 1368 369 1219T248 801V649ZM1137 807Q1137 1018 1057 1160L434 171Q541 87 694 87Q903 87 1020 236T1137 653V807Z" />
<glyph unicode="&#xd9;" horiz-adv-x="1346" d="M1187 1456V462Q1186 315 1122 206T942 39T674 -20Q444 -20 306 105T162 453V1456H284V471Q284 287 389 186T674 84T958 186T1063 470V1456H1187ZM756 1571H647L421 1851H565L756 1571Z" />
<glyph unicode="&#xda;" horiz-adv-x="1346" d="M1187 1456V462Q1186 315 1122 206T942 39T674 -20Q444 -20 306 105T162 453V1456H284V471Q284 287 389 186T674 84T958 186T1063 470V1456H1187ZM802 1851H947L716 1571H611L802 1851Z" />
<glyph unicode="&#xdb;" horiz-adv-x="1346" d="M1187 1456V462Q1186 315 1122 206T942 39T674 -20Q444 -20 306 105T162 453V1456H284V471Q284 287 389 186T674 84T958 186T1063 470V1456H1187ZM961 1583V1573H850L682 1756L515 1573H406V1586L644 1841H720L961 1583Z" />
<glyph unicode="&#xdc;" horiz-adv-x="1346" d="M1187 1456V462Q1186 315 1122 206T942 39T674 -20Q444 -20 306 105T162 453V1456H284V471Q284 287 389 186T674 84T958 186T1063 470V1456H1187ZM383 1711Q383 1745 405 1768T465 1792T525 1769T548 1711Q548 1678
526 1655T465 1632T405 1655T383 1711ZM821 1710Q821 1744 843 1767T903 1791T963 1768T986 1710Q986 1677 964 1654T903 1631T843 1654T821 1710Z" />
<glyph unicode="&#xdd;" horiz-adv-x="1226" d="M611 662L1056 1456H1198L672 548V0H549V548L24 1456H170L611 662ZM732 1845H877L646 1565H541L732 1845Z" />
<glyph unicode="&#xde;" horiz-adv-x="1214" d="M303 1456V1152H628Q771 1152 877 1101T1039 956T1096 738Q1096 553 974 441T641 324H303V0H183V1456H303ZM303 1051V425H627Q784 425 880 510T976 736T885 961T642 1051H303Z" />
<glyph unicode="&#xdf;" horiz-adv-x="1200" d="M271 0H151V1127Q151 1327 246 1435T512 1544Q665 1544 760 1460T856 1237Q856 1179 843 1131T794 1019T746 913T733 824Q733 768 774 716T911 593T1051 454T1096 306Q1096 160 990 70T720 -20Q636 -20 545 4T414
60L448 161Q485 132 562 106T706 80Q828 80 902 144T976 306Q976 367 932 423T797 547T659 681T613 826Q613 922 676 1034T739 1230Q739 1323 676 1382T522 1442Q275 1442 271 1136V0Z" />
<glyph unicode="&#xe0;" horiz-adv-x="1097" d="M839 0Q821 51 816 151Q753 69 656 25T449 -20Q293 -20 197 67T100 287Q100 445 231 537T598 629H815V752Q815 868 744 934T535 1001Q410 1001 328 937T246 783L126 784Q126 913 246 1007T541 1102Q722 1102 826
1012T934 759V247Q934 90 967 12V0H839ZM463 86Q583 86 677 144T815 299V537H601Q422 535 321 472T220 297Q220 206 287 146T463 86ZM653 1256H544L318 1536H462L653 1256Z" />
<glyph unicode="&#xe1;" horiz-adv-x="1097" d="M839 0Q821 51 816 151Q753 69 656 25T449 -20Q293 -20 197 67T100 287Q100 445 231 537T598 629H815V752Q815 868 744 934T535 1001Q410 1001 328 937T246 783L126 784Q126 913 246 1007T541 1102Q722 1102 826
1012T934 759V247Q934 90 967 12V0H839ZM463 86Q583 86 677 144T815 299V537H601Q422 535 321 472T220 297Q220 206 287 146T463 86ZM699 1536H844L613 1256H508L699 1536Z" />
<glyph unicode="&#xe2;" horiz-adv-x="1097" d="M839 0Q821 51 816 151Q753 69 656 25T449 -20Q293 -20 197 67T100 287Q100 445 231 537T598 629H815V752Q815 868 744 934T535 1001Q410 1001 328 937T246 783L126 784Q126 913 246 1007T541 1102Q722 1102 826
1012T934 759V247Q934 90 967 12V0H839ZM463 86Q583 86 677 144T815 299V537H601Q422 535 321 472T220 297Q220 206 287 146T463 86ZM858 1268V1258H747L579 1441L412 1258H303V1271L541 1526H617L858 1268Z" />
<glyph unicode="&#xe3;" horiz-adv-x="1097" d="M839 0Q821 51 816 151Q753 69 656 25T449 -20Q293 -20 197 67T100 287Q100 445 231 537T598 629H815V752Q815 868 744 934T535 1001Q410 1001 328 937T246 783L126 784Q126 913 246 1007T541 1102Q722 1102 826
1012T934 759V247Q934 90 967 12V0H839ZM463 86Q583 86 677 144T815 299V537H601Q422 535 321 472T220 297Q220 206 287 146T463 86ZM920 1494Q920 1398 864 1340T725 1281Q649 1281 577 1336T447 1391Q400 1391 369 1360T337 1273L247 1276Q247 1368 301 1428T442
1488Q490 1488 524 1471T588 1433T648 1395T720 1378Q766 1378 798 1411T831 1500L920 1494Z" />
<glyph unicode="&#xe4;" horiz-adv-x="1097" d="M839 0Q821 51 816 151Q753 69 656 25T449 -20Q293 -20 197 67T100 287Q100 445 231 537T598 629H815V752Q815 868 744 934T535 1001Q410 1001 328 937T246 783L126 784Q126 913 246 1007T541 1102Q722 1102 826
1012T934 759V247Q934 90 967 12V0H839ZM463 86Q583 86 677 144T815 299V537H601Q422 535 321 472T220 297Q220 206 287 146T463 86ZM280 1396Q280 1430 302 1453T362 1477T422 1454T445 1396Q445 1363 423 1340T362 1317T302 1340T280 1396ZM718 1395Q718 1429
740 1452T800 1476T860 1453T883 1395Q883 1362 861 1339T800 1316T740 1339T718 1395Z" />
<glyph unicode="&#xe5;" horiz-adv-x="1097" d="M839 0Q821 51 816 151Q753 69 656 25T449 -20Q293 -20 197 67T100 287Q100 445 231 537T598 629H815V752Q815 868 744 934T535 1001Q410 1001 328 937T246 783L126 784Q126 913 246 1007T541 1102Q722 1102 826
1012T934 759V247Q934 90 967 12V0H839ZM463 86Q583 86 677 144T815 299V537H601Q422 535 321 472T220 297Q220 206 287 146T463 86ZM387 1400Q387 1480 443 1535T580 1590Q659 1590 716 1535T773 1400Q773 1321 718 1267T580 1213T442 1267T387 1400ZM464 1400Q464
1350 497 1317T580 1284Q629 1284 663 1316T697 1400Q697 1453 662 1486T580 1519Q531 1519 498 1485T464 1400Z" />
<glyph unicode="&#xe6;" horiz-adv-x="1732" d="M1265 -20Q1126 -20 1027 34T867 186Q807 88 693 34T440 -20Q271 -20 178 64T85 293Q85 450 195 539T511 632H781V720Q781 852 718 926T528 1000Q398 1000 315 935T232 765L113 778Q113 922 229 1012T528 1102Q653
1102 741 1049T870 889Q930 989 1024 1045T1235 1102Q1431 1102 1543 982T1658 644V538H901V509Q901 308 997 195T1265 81Q1450 81 1589 199L1636 112Q1491 -20 1265 -20ZM458 80Q549 80 642 126T781 236V536H525Q388 536 302 475T207 309L206 289Q206 192 271
136T458 80ZM1235 1000Q1103 1000 1013 902T904 636H1539V667Q1539 821 1459 910T1235 1000Z" />
<glyph unicode="&#xe7;" horiz-adv-x="1055" d="M556 81Q681 81 765 151T857 334H972Q967 235 910 154T759 26T556 -20Q343 -20 219 128T94 526V562Q94 722 150 845T310 1035T555 1102Q733 1102 848 996T972 717H857Q849 844 766 922T555 1000Q393 1000 304 883T214
555V520Q214 313 303 197T556 81ZM589 -9L577 -73Q742 -94 742 -244Q742 -336 663 -390T448 -444L441 -366Q530 -366 586 -334T643 -246Q643 -188 600 -166T467 -136L496 -9H589Z" />
<glyph unicode="&#xe8;" horiz-adv-x="1058" d="M575 -20Q437 -20 326 48T152 237T90 510V553Q90 709 150 834T319 1030T553 1102Q750 1102 865 968T981 600V533H209V510Q209 326 314 204T580 81Q676 81 749 116T883 228L958 171Q826 -20 575 -20ZM553 1000Q418
1000 326 901T213 635H862V648Q857 804 773 902T553 1000ZM640 1256H531L305 1536H449L640 1256Z" />
<glyph unicode="&#xe9;" horiz-adv-x="1058" d="M575 -20Q437 -20 326 48T152 237T90 510V553Q90 709 150 834T319 1030T553 1102Q750 1102 865 968T981 600V533H209V510Q209 326 314 204T580 81Q676 81 749 116T883 228L958 171Q826 -20 575 -20ZM553 1000Q418
1000 326 901T213 635H862V648Q857 804 773 902T553 1000ZM686 1536H831L600 1256H495L686 1536Z" />
<glyph unicode="&#xea;" horiz-adv-x="1058" d="M575 -20Q437 -20 326 48T152 237T90 510V553Q90 709 150 834T319 1030T553 1102Q750 1102 865 968T981 600V533H209V510Q209 326 314 204T580 81Q676 81 749 116T883 228L958 171Q826 -20 575 -20ZM553 1000Q418
1000 326 901T213 635H862V648Q857 804 773 902T553 1000ZM845 1268V1258H734L566 1441L399 1258H290V1271L528 1526H604L845 1268Z" />
<glyph unicode="&#xeb;" horiz-adv-x="1058" d="M575 -20Q437 -20 326 48T152 237T90 510V553Q90 709 150 834T319 1030T553 1102Q750 1102 865 968T981 600V533H209V510Q209 326 314 204T580 81Q676 81 749 116T883 228L958 171Q826 -20 575 -20ZM553 1000Q418
1000 326 901T213 635H862V648Q857 804 773 902T553 1000ZM267 1396Q267 1430 289 1453T349 1477T409 1454T432 1396Q432 1363 410 1340T349 1317T289 1340T267 1396ZM705 1395Q705 1429 727 1452T787 1476T847 1453T870 1395Q870 1362 848 1339T787 1316T727 1339T705
1395Z" />
<glyph unicode="&#xec;" horiz-adv-x="456" d="M288 0H168V1082H288V0ZM305 1244H196L-30 1524H114L305 1244Z" />
<glyph unicode="&#xed;" horiz-adv-x="456" d="M288 0H168V1082H288V0ZM350 1780H495L264 1500H159L350 1780Z" />
<glyph unicode="&#xee;" horiz-adv-x="456" d="M288 0H168V1082H288V0ZM510 1256V1246H399L231 1429L64 1246H-45V1259L193 1514H269L510 1256Z" />
<glyph unicode="&#xef;" horiz-adv-x="456" d="M288 0H168V1082H288V0ZM-68 1384Q-68 1418 -46 1441T14 1465T74 1442T97 1384Q97 1351 75 1328T14 1305T-46 1328T-68 1384ZM370 1383Q370 1417 392 1440T452 1464T512 1441T535 1383Q535 1350 513 1327T452 1304T392
1327T370 1383Z" />
<glyph unicode="&#xf0;" horiz-adv-x="1191" d="M811 1303Q1049 1053 1055 645V535Q1055 376 999 249T842 51T615 -20Q485 -20 379 41T211 216T149 466Q149 695 268 830T587 965Q687 965 773 927T919 821Q877 1072 709 1240L484 1101L433 1174L639 1302Q502 1408
296 1475L335 1578Q577 1506 744 1366L938 1487L989 1414L811 1303ZM935 625L933 682Q894 765 807 813T609 861Q448 861 359 756T269 466Q269 363 314 274T438 134T619 83Q760 83 847 207T935 543V625Z" />
<glyph unicode="&#xf1;" horiz-adv-x="1125" d="M270 1082L274 897Q335 997 426 1049T627 1102Q801 1102 886 1004T972 710V0H853V711Q852 856 792 927T598 998Q487 998 402 929T275 741V0H156V1082H270ZM916 1493Q916 1397 860 1339T721 1280Q645 1280 573 1335T443
1390Q396 1390 365 1359T333 1272L243 1275Q243 1367 297 1427T438 1487Q486 1487 520 1470T584 1432T644 1394T716 1377Q762 1377 794 1410T827 1499L916 1493Z" />
<glyph unicode="&#xf2;" horiz-adv-x="1147" d="M90 557Q90 713 150 838T321 1032T572 1102Q788 1102 922 951T1056 549V524Q1056 367 996 242T825 48T574 -20Q359 -20 225 131T90 533V557ZM210 524Q210 330 310 206T574 81Q736 81 836 205T937 534V557Q937 681
891 784T762 943T572 1000Q412 1000 311 875T210 546V524ZM645 1256H536L310 1536H454L645 1256Z" />
<glyph unicode="&#xf3;" horiz-adv-x="1147" d="M90 557Q90 713 150 838T321 1032T572 1102Q788 1102 922 951T1056 549V524Q1056 367 996 242T825 48T574 -20Q359 -20 225 131T90 533V557ZM210 524Q210 330 310 206T574 81Q736 81 836 205T937 534V557Q937 681
891 784T762 943T572 1000Q412 1000 311 875T210 546V524ZM691 1536H836L605 1256H500L691 1536Z" />
<glyph unicode="&#xf4;" horiz-adv-x="1147" d="M90 557Q90 713 150 838T321 1032T572 1102Q788 1102 922 951T1056 549V524Q1056 367 996 242T825 48T574 -20Q359 -20 225 131T90 533V557ZM210 524Q210 330 310 206T574 81Q736 81 836 205T937 534V557Q937 681
891 784T762 943T572 1000Q412 1000 311 875T210 546V524ZM850 1268V1258H739L571 1441L404 1258H295V1271L533 1526H609L850 1268Z" />
<glyph unicode="&#xf5;" horiz-adv-x="1147" d="M90 557Q90 713 150 838T321 1032T572 1102Q788 1102 922 951T1056 549V524Q1056 367 996 242T825 48T574 -20Q359 -20 225 131T90 533V557ZM210 524Q210 330 310 206T574 81Q736 81 836 205T937 534V557Q937 681
891 784T762 943T572 1000Q412 1000 311 875T210 546V524ZM912 1493Q912 1397 856 1339T717 1280Q641 1280 569 1335T439 1390Q392 1390 361 1359T329 1272L239 1275Q239 1367 293 1427T434 1487Q482 1487 516 1470T580 1432T640 1394T712 1377Q758 1377 790 1410T823
1499L912 1493Z" />
<glyph unicode="&#xf6;" horiz-adv-x="1147" d="M90 557Q90 713 150 838T321 1032T572 1102Q788 1102 922 951T1056 549V524Q1056 367 996 242T825 48T574 -20Q359 -20 225 131T90 533V557ZM210 524Q210 330 310 206T574 81Q736 81 836 205T937 534V557Q937 681
891 784T762 943T572 1000Q412 1000 311 875T210 546V524ZM272 1396Q272 1430 294 1453T354 1477T414 1454T437 1396Q437 1363 415 1340T354 1317T294 1340T272 1396ZM710 1395Q710 1429 732 1452T792 1476T852 1453T875 1395Q875 1362 853 1339T792 1316T732 1339T710
1395Z" />
<glyph unicode="&#xf7;" horiz-adv-x="1164" d="M1070 644H72V760H1070V644ZM495 1088Q495 1123 517 1147T579 1171T642 1147T665 1088T642 1030T579 1006T517 1029T495 1088ZM495 291Q495 326 517 350T579 374T642 350T665 291T642 233T579 210T517 233T495 291Z" />
<glyph unicode="&#xf8;" horiz-adv-x="1140" d="M89 557Q89 713 149 838T320 1032T571 1102Q685 1102 785 1054L863 1214H957L857 1010Q951 938 1003 821T1055 557V524Q1055 368 994 242T823 48T573 -20Q465 -20 373 21L294 -140H200L299 63Q199 134 144 253T89
524V557ZM208 524Q208 414 243 319T348 163L737 957Q662 1000 571 1000Q410 1000 309 875T208 546V524ZM935 557Q935 660 902 751T806 905L419 115Q487 81 573 81Q734 81 834 205T935 534V557Z" />
<glyph unicode="&#xf9;" horiz-adv-x="1125" d="M852 137Q744 -20 507 -20Q334 -20 244 80T152 378V1082H271V393Q271 84 521 84Q781 84 850 299V1082H970V0H854L852 137ZM647 1256H538L312 1536H456L647 1256Z" />
<glyph unicode="&#xfa;" horiz-adv-x="1125" d="M852 137Q744 -20 507 -20Q334 -20 244 80T152 378V1082H271V393Q271 84 521 84Q781 84 850 299V1082H970V0H854L852 137ZM693 1536H838L607 1256H502L693 1536Z" />
<glyph unicode="&#xfb;" horiz-adv-x="1125" d="M852 137Q744 -20 507 -20Q334 -20 244 80T152 378V1082H271V393Q271 84 521 84Q781 84 850 299V1082H970V0H854L852 137ZM852 1268V1258H741L573 1441L406 1258H297V1271L535 1526H611L852 1268Z" />
<glyph unicode="&#xfc;" horiz-adv-x="1125" d="M852 137Q744 -20 507 -20Q334 -20 244 80T152 378V1082H271V393Q271 84 521 84Q781 84 850 299V1082H970V0H854L852 137ZM274 1396Q274 1430 296 1453T356 1477T416 1454T439 1396Q439 1363 417 1340T356 1317T296
1340T274 1396ZM712 1395Q712 1429 734 1452T794 1476T854 1453T877 1395Q877 1362 855 1339T794 1316T734 1339T712 1395Z" />
<glyph unicode="&#xfd;" horiz-adv-x="973" d="M499 172L815 1082H944L482 -184L458 -240Q369 -437 183 -437Q140 -437 91 -423L90 -324L152 -330Q240 -330 294 -287T387 -137L440 9L32 1082H163L499 172ZM633 1536H778L547 1256H442L633 1536Z" />
<glyph unicode="&#xfe;" horiz-adv-x="1150" d="M1031 530Q1031 277 919 129T618 -20Q397 -20 279 136V-416H159V1536H279V932Q396 1102 616 1102Q808 1102 919 956T1031 548V530ZM911 551Q911 758 826 878T589 998Q479 998 400 945T279 791V270Q321 180 400 131T591
81Q742 81 826 201T911 551Z" />
<glyph unicode="&#xff;" horiz-adv-x="973" d="M499 172L815 1082H944L482 -184L458 -240Q369 -437 183 -437Q140 -437 91 -423L90 -324L152 -330Q240 -330 294 -287T387 -137L440 9L32 1082H163L499 172ZM214 1396Q214 1430 236 1453T296 1477T356 1454T379 1396Q379
1363 357 1340T296 1317T236 1340T214 1396ZM652 1395Q652 1429 674 1452T734 1476T794 1453T817 1395Q817 1362 795 1339T734 1316T674 1339T652 1395Z" />
<glyph unicode="&#x2013;" horiz-adv-x="1334" d="M1417 686H415V788H1417V686Z" />
<glyph unicode="&#x2014;" horiz-adv-x="1580" d="M1462 686H126V788H1462V686Z" />
<glyph unicode="&#x2018;" horiz-adv-x="364" d="M238 1554L310 1503Q220 1385 217 1249V1121H98V1233Q98 1325 135 1410T238 1554Z" />
<glyph unicode="&#x2019;" horiz-adv-x="364" d="M133 1099L62 1151Q152 1272 155 1405V1536H273V1435Q273 1226 133 1099Z" />
<glyph unicode="&#x201a;" horiz-adv-x="353" d="M112 -231L41 -179Q124 -68 132 51L133 205H252V104Q252 -104 112 -231Z" />
<glyph unicode="&#x201c;" horiz-adv-x="612" d="M239 1554L311 1503Q221 1385 218 1249V1121H99V1233Q99 1325 136 1410T239 1554ZM490 1554L562 1503Q472 1385 469 1249V1121H350V1233Q350 1325 387 1410T490 1554Z" />
<glyph unicode="&#x201d;" horiz-adv-x="617" d="M139 1099L68 1151Q158 1272 161 1405V1536H279V1435Q279 1226 139 1099ZM383 1099L312 1151Q402 1272 405 1405V1536H523V1435Q523 1226 383 1099Z" />
<glyph unicode="&#x201e;" horiz-adv-x="593" d="M112 -240L41 -188Q130 -65 133 73V236H252V106Q252 -111 112 -240ZM346 -240L275 -188Q363 -66 366 73V236H486V106Q486 -111 346 -240Z" />
<glyph unicode="&#x2022;" horiz-adv-x="662" d="M146 752Q146 831 197 881T331 931Q413 931 464 883T517 757V717Q517 636 466 588T332 540Q248 540 197 589T146 719V752Z" />
<glyph unicode="&#x2039;" horiz-adv-x="609" d="M232 555L508 167H403L108 546V565L403 944H508L232 555Z" />
<glyph unicode="&#x203a;" horiz-adv-x="609" d="M203 944L498 560V541L203 162H97L373 550L97 944H203Z" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,350 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<defs >
<font id="UbuntuMono" horiz-adv-x="500" ><font-face
font-family="Ubuntu Mono"
units-per-em="1000"
panose-1="2 11 5 9 3 6 2 3 2 4"
ascent="830"
descent="-170"
alphabetic="0" />
<glyph unicode=" " glyph-name="space" />
<glyph unicode="!" glyph-name="exclam" d="M293 451Q293 414 292 383T289 324T283 269T277 211H220Q217 241 214 268T209 324T205 383T204 451V619H293V451ZM248 -12Q221 -12 201 6T181 56Q181 87 201 105T248 124Q276 124 295 106T315 56Q315 25 296 7T248 -12Z" />
<glyph unicode="&quot;" glyph-name="quotedbl" d="M206 679V634Q206 590 201 534T188 430H146Q139 477 134 533T129 635V679H206ZM371 679V634Q371 590 366 534T354 430H312Q304 477 299 533T294 635V679H371Z" />
<glyph unicode="#" glyph-name="numbersign" d="M321 0H246L277 163H166L135 0H60L91 163H27V230H104L134 389H27V455H148L179 619H254L223 455H334L364 619H439L409 455H473V389H396L365 230H473V163H352L321 0ZM320 389H210L179 230H289L320 389Z" />
<glyph unicode="$" glyph-name="dollar" d="M233 85Q269 85 293 90T332 106T352 131T358 163Q358 188 346 205T314 236T267 260T213 281Q186 291 160 303T113 332T79 374T66 435Q66 499 104 539T214 590V693H288V593Q328 591 361 584T415 568L398 498Q377 506
345 514T264 523Q210 523 182 503T153 445Q153 424 161 410T186 385T223 365T272 346Q306 333 338 319T394 284T432 236T447 169Q447 110 408 69T288 18V-97H214V15Q151 17 112 29T54 52L76 121Q103 108 140 97T233 85Z" />
<glyph unicode="%" glyph-name="percent" d="M95 0H25L404 619H474L95 0ZM18 477Q18 553 47 592T127 632Q178 632 207 593T236 477Q236 401 207 361T127 321Q76 321 47 361T18 477ZM174 477Q174 522 162 551T127 580Q104 580 92 551T79 477Q79 432 91 403T127
374Q150 374 162 403T174 477ZM265 142Q265 218 293 257T373 297Q424 297 453 258T482 142Q482 66 453 27T373 -13Q322 -13 294 26T265 142ZM421 142Q421 187 409 216T373 245Q350 245 338 216T326 142Q326 97 338 68T373 39Q396 39 408 68T421 142Z" />
<glyph unicode="&amp;" glyph-name="ampersand" d="M272 283Q289 263 307 243T345 199Q353 227 359 260T368 333L437 324Q431 266 420 221T391 138Q414 106 435 72T473 0H386Q369 35 348 67Q316 27 277 10T194 -8Q156 -8 126 4T75 39T43 90T31 151Q31 198 55 246T133
335Q107 370 91 406T75 480Q75 519 87 548T121 595T169 623T226 633Q252 633 277 625T321 601T351 560T363 505Q363 459 333 411T238 321L272 283ZM172 285Q143 259 129 225T114 159Q114 120 134 95T183 63T246 70T308 125Q275 170 240 208T172 285ZM288 496Q288
532 269 550T224 568Q198 568 176 548T153 486Q153 457 162 432T200 369Q248 399 268 433T288 496Z" />
<glyph unicode="&apos;" glyph-name="quotesingle" d="M293 679V634Q293 612 292 584T288 525T282 465T275 412H224Q220 435 217 464T212 524T208 584T206 635V679H293Z" />
<glyph unicode="(" glyph-name="parenleft" d="M384 641Q291 569 242 475T193 270Q193 214 204 164T239 68T299 -21T386 -107L341 -167Q226 -82 170 30T113 268Q113 392 170 505T341 701L385 641H384Z" />
<glyph unicode=")" glyph-name="parenright" d="M115 -107Q208 -35 257 59T307 264Q307 320 296 370T260 466T200 555T113 641L158 701Q273 616 329 504T386 266Q386 204 372 144T329 29T258 -76T158 -167L114 -107H115Z" />
<glyph unicode="*" glyph-name="asterisk" d="M287 417Q314 392 343 365T392 306L396 301L326 250L321 256Q300 286 283 321T251 390Q235 357 218 322T178 256L173 251L104 301L109 307Q131 338 159 365T214 417Q177 422 138 427T64 445L57 447L84 529L91 526Q126
512 161 495T229 460Q221 496 215 535T208 612V619H294V612Q294 574 288 535T273 460Q305 479 340 495T410 525L417 528L443 446L437 444Q401 433 363 427T287 417Z" />
<glyph unicode="+" glyph-name="plus" d="M46 298H214V482H286V298H455V228H286V43H214V228H46V298Z" />
<glyph unicode="," glyph-name="comma" d="M149 -79Q167 -75 184 -71T217 -60T244 -41T263 -9Q231 -6 217 15T202 58Q202 98 224 118T276 139Q313 139 332 113T351 48Q351 19 340 -12T305 -71T246 -118T163 -144L149 -79Z" />
<glyph unicode="-" glyph-name="hyphen" d="M140 293H360V215H140V293Z" />
<glyph unicode="." glyph-name="period" d="M324 64Q324 34 304 11T251 -12Q217 -12 197 11T177 64Q177 95 197 118T251 141Q284 141 304 118T324 64Z" />
<glyph unicode="/" glyph-name="slash" d="M154 -165H69L348 699H431L154 -165Z" />
<glyph unicode="0" glyph-name="zero" d="M308 321Q308 295 293 276T252 257Q226 257 210 276T194 321Q194 347 210 367T252 387Q277 387 292 367T308 321ZM46 310Q46 466 99 549T250 633Q348 633 401 550T454 310Q454 154 401 71T250 -13Q153 -13 100 70T46 310ZM370
310Q370 361 364 406T344 486T307 540T250 560Q216 560 193 540T156 486T136 407T130 310Q130 259 136 214T156 134T193 80T250 60Q284 60 307 80T344 134T364 213T370 310Z" />
<glyph unicode="1" glyph-name="one" d="M75 491Q126 511 174 541T263 619H321V70H438V0H105V70H239V504Q228 494 213 484T179 463T140 444T101 429L75 491Z" />
<glyph unicode="2" glyph-name="two" d="M420 461Q420 429 408 399T375 340T328 283T275 228Q260 213 240 192T202 149T172 106T160 70H445V0H70Q69 5 69 10T69 21Q69 63 83 99T119 167T168 227T223 283Q245 305 265 326T301 368T326 411T336 458Q336 485 328
504T305 535T271 554T230 560Q204 560 183 553T145 536T116 517T98 501L57 559Q65 568 81 581T119 605T170 625T230 633Q327 633 373 589T420 461Z" />
<glyph unicode="3" glyph-name="three" d="M212 60Q291 60 324 91T358 176Q358 210 344 233T307 270T254 290T191 296H170V363H199Q221 363 244 367T287 383T319 415T331 466Q331 517 300 538T226 560Q183 560 153 548T103 522L71 585Q92 600 134 616T229 633Q278
633 313 621T370 587T404 536T415 471Q415 422 390 388T324 336Q372 322 407 282T442 174Q442 134 429 100T388 40T317 1T213 -13Q190 -13 166 -10T120 -1T83 10T59 19L75 90Q91 82 126 71T212 60Z" />
<glyph unicode="4" glyph-name="four" d="M36 214Q53 254 82 306T149 413T228 522T313 619H393V226H466V158H393V0H313V158H36V214ZM313 521Q286 492 259 457T205 384T157 305T116 226H313V521Z" />
<glyph unicode="5" glyph-name="five" d="M186 380Q320 375 381 321T442 176Q442 135 429 100T389 40T319 1T218 -13Q194 -13 171 -10T126 -1T89 9T66 19L82 90Q98 82 131 71T216 60Q256 60 283 69T326 93T350 129T358 171Q358 205 347 231T307 276T229 303T104
313Q110 357 113 395T119 470T123 543T127 619H425V549H199Q198 535 197 513T193 465T189 417T186 380Z" />
<glyph unicode="6" glyph-name="six" d="M54 248Q54 338 78 407T148 524T259 596T406 622L413 552Q360 551 317 541T239 507T182 446T147 355Q171 366 199 373T260 380Q312 380 348 364T407 322T439 260T449 188Q449 154 438 119T404 54T345 6T259 -13Q156 -13
105 56T54 248ZM251 312Q219 312 193 306T139 288Q138 278 138 269T138 248Q138 209 143 175T162 114T199 72T258 56Q288 56 308 68T341 100T359 142T365 187Q365 248 338 280T251 312Z" />
<glyph unicode="7" glyph-name="seven" d="M155 0Q160 71 180 150T230 304T294 442T363 547H63V619H455V550Q425 515 390 456T324 324T269 167T240 0H155Z" />
<glyph unicode="8" glyph-name="eight" d="M449 161Q449 83 400 35T249 -13Q191 -13 153 2T93 42T61 97T51 156Q51 209 80 250T149 316Q64 364 64 463Q64 497 77 528T114 582T172 619T250 633Q300 633 335 618T393 580T425 528T435 472Q435 419 408 381T346 319Q449
270 449 161ZM131 155Q131 139 137 122T157 89T194 65T250 55Q281 55 303 63T340 86T362 119T369 155Q369 212 328 242T215 289Q175 267 153 234T131 155ZM355 472Q355 485 349 501T331 531T298 554T250 564Q221 564 202 555T169 533T151 503T145 470Q145 429 174
394T272 342Q310 364 332 394T355 472Z" />
<glyph unicode="9" glyph-name="nine" d="M447 371Q447 186 357 93T86 -2L83 68Q195 68 263 112T354 266Q330 255 301 249T240 242Q187 242 151 257T93 299T61 360T51 432Q51 466 62 501T96 566T155 614T241 633Q343 633 395 563T447 371ZM249 310Q281 310 308
316T362 333Q363 343 363 352T363 371Q363 410 358 445T339 506T302 547T242 563Q212 563 192 551T159 520T141 478T135 434Q135 373 162 342T249 310Z" />
<glyph unicode=":" glyph-name="colon" d="M324 64Q324 34 304 11T251 -12Q217 -12 197 11T177 64Q177 95 197 118T251 141Q284 141 304 118T324 64ZM324 387Q324 357 304 334T251 311Q217 311 197 334T177 387Q177 418 197 441T251 464Q284 464 304 441T324 387Z" />
<glyph unicode=";" glyph-name="semicolon" d="M381 -79Q399 -75 416 -71T449 -60T476 -41T495 -9Q463 -6 449 15T434 58Q434 98 456 118T508 139Q545 139 564 113T583 48Q583 19 572 -12T537 -71T478 -118T395 -144L381 -79ZM324 387Q324 357 304 334T251 311Q217
311 197 334T177 387Q177 418 197 441T251 464Q284 464 304 441T324 387Z" />
<glyph unicode="&lt;" glyph-name="less" d="M141 255L458 124L435 55L49 219V290L435 454L458 385L141 255Z" />
<glyph unicode="=" glyph-name="equal" d="M46 203H455V132H46V203ZM46 393H455V322H46V393Z" />
<glyph unicode="&gt;" glyph-name="greater" d="M49 385L72 454L458 290V219L72 55L49 124L366 255L49 385Z" />
<glyph unicode="?" glyph-name="question" d="M185 211L184 230Q184 273 206 304T254 363T303 417T325 479Q325 517 301 540T230 563Q201 563 173 556T115 533L92 597Q122 614 160 623T241 633Q292 633 324 619T376 584T402 536T410 486Q410 456 399 433T370 388T333
348T296 309T268 264T256 211H185ZM228 -12Q200 -12 181 6T161 56Q161 87 180 105T228 124Q255 124 275 106T295 56Q295 25 275 7T228 -12Z" />
<glyph unicode="@" glyph-name="at" d="M468 63Q438 51 407 47T350 42Q315 42 285 53T233 89T198 150T185 240Q185 333 233 381T358 430Q366 430 373 430T389 428Q389 491 363 528T280 565Q245 565 216 548T166 494T133 397T121 254Q121 191 133 133T170 30T235
-40T330 -67Q370 -67 413 -54L421 -121Q368 -135 323 -135Q250 -135 197 -103T109 -17T57 104T40 243Q40 346 58 420T109 541T185 610T278 633Q366 633 417 574T468 408V63ZM265 240Q265 220 267 197T279 155T304 122T350 108Q358 108 368 109T389 113V358Q380
361 372 362T355 363Q312 363 289 330T265 240Z" />
<glyph unicode="A" glyph-name="A" d="M404 0L367 162H129L93 0H9Q26 66 47 143T92 301T144 463T201 619H303Q332 545 358 464T408 301T453 143T491 0H404ZM249 547Q226 484 199 401T148 230H348Q325 320 299 403T249 547Z" />
<glyph unicode="B" glyph-name="B" d="M204 -5Q189 -5 169 -4T129 -2T89 3T54 10V610Q69 614 88 617T128 621T168 624T204 625Q250 625 290 617T359 590T405 540T422 463Q422 441 415 421T396 382T367 351T331 330Q382 316 416 279T451 181Q451 89 392 42T204
-5ZM136 289V68Q138 67 158 66T210 64Q241 64 269 69T320 87T355 123T369 180Q369 211 357 232T324 265T276 283T220 289H136ZM136 357H201Q226 357 251 362T296 378T328 408T340 456Q340 483 329 502T300 533T259 550T209 555Q183 555 164 555T136 552V357Z" />
<glyph unicode="C" glyph-name="C" d="M460 26Q425 6 386 -3T301 -13Q246 -13 200 6T119 65T66 166T46 310Q46 389 66 449T122 550T204 612T304 633Q341 633 379 623T455 590L431 522Q365 560 307 560Q266 560 234 543T178 492T143 413T130 310Q130 246 143 199T181
121T238 75T311 60Q340 60 372 67T438 94L460 26Z" />
<glyph unicode="D" glyph-name="D" d="M460 310Q460 224 439 164T380 67T291 12T179 -5Q117 -5 54 10V610Q117 625 179 625Q239 625 290 608T379 553T438 455T460 310ZM136 69Q161 66 187 66Q232 66 267 79T326 122T363 198T376 310Q376 436 328 495T184 554Q171
554 159 554T136 551V69Z" />
<glyph unicode="E" glyph-name="E" d="M91 0V619H438V549H173V359H405V289H173V70H460V0H91Z" />
<glyph unicode="F" glyph-name="F" d="M91 0V619H442V549H173V356H410V287H173V0H91Z" />
<glyph unicode="G" glyph-name="G" d="M373 305H455V15Q436 8 394 -2T295 -13Q239 -13 193 8T115 71T64 172T46 310Q46 388 66 448T120 549T201 611T300 633Q334 633 360 628T404 616T435 602T453 590L426 521Q403 539 372 550T305 562Q267 562 235 544T179 493T143
414T130 310Q130 254 141 208T175 129T229 78T304 59Q333 59 349 63T373 71V305Z" />
<glyph unicode="H" glyph-name="H" d="M373 619H455V0H373V288H127V0H45V619H127V358H373V619Z" />
<glyph unicode="I" glyph-name="I" d="M291 549V70H411V0H89V70H209V549H89V619H411V549H291Z" />
<glyph unicode="J" glyph-name="J" d="M127 549V619H420V199Q420 155 411 116T378 49T315 4T215 -13Q155 -13 114 4T54 37L87 104Q106 90 137 74T211 58Q275 58 306 92T338 207V549H127Z" />
<glyph unicode="K" glyph-name="K" d="M470 619Q421 546 363 474T236 330Q268 303 304 267T374 189T439 98T491 0H398Q373 46 344 90T283 172T216 241T146 296V0H64V619H146V343Q211 410 270 483T375 619H470Z" />
<glyph unicode="L" glyph-name="L" d="M460 70V0H91V619H173V70H460Z" />
<glyph unicode="M" glyph-name="M" d="M251 311L347 619H424Q444 473 452 320T468 0H388Q387 54 386 117T384 247T381 383T377 517L287 235H215L123 517Q122 452 121 384T118 248T115 117T112 0H32Q35 76 39 156T48 315T61 471T78 619H154L251 311Z" />
<glyph unicode="N" glyph-name="N" d="M361 0Q327 83 297 150T240 277T185 388T130 494V0H55V619H139Q180 544 210 488T266 380T316 272T370 143V619H445V0H361Z" />
<glyph unicode="O" glyph-name="O" d="M29 310Q29 393 45 454T90 554T159 613T250 633Q299 633 340 614T410 555T455 454T472 310Q472 227 456 166T410 65T340 6T250 -13Q200 -13 160 6T90 65T45 166T29 310ZM113 310Q113 188 146 123T248 58Q318 58 353 123T388
310Q388 432 353 497T248 562Q179 562 146 497T113 310Z" />
<glyph unicode="P" glyph-name="P" d="M235 625Q290 625 331 611T400 572T441 511T455 433Q455 336 398 284T227 231H155V0H73V610Q91 614 112 617T155 622T197 624T235 625ZM241 554Q215 554 191 554T155 551V301H223Q298 301 334 331T371 430Q371 554 241 554Z" />
<glyph unicode="Q" glyph-name="Q" d="M29 309Q29 392 45 453T90 553T159 612T250 632Q299 632 340 613T410 554T455 453T472 309Q472 235 459 179T422 82T364 19T289 -11Q292 -31 306 -45T341 -70T390 -88T448 -99L429 -165Q388 -158 351 -147T283 -118T232 -74T204
-10Q123 7 76 84T29 309ZM113 309Q113 187 146 122T248 57Q318 57 353 122T388 309Q388 431 353 496T248 561Q179 561 146 496T113 309Z" />
<glyph unicode="R" glyph-name="R" d="M431 430Q431 373 401 330T320 262Q335 238 353 208T391 143T428 72T463 0H375Q345 72 310 138T241 245Q235 244 224 244T208 244H138V0H55V610Q70 614 89 617T129 621T170 624T206 625Q318 625 374 576T431 430ZM213 554Q190
554 169 554T138 551V312H191Q227 312 255 317T304 335T336 372T347 434Q347 470 336 493T307 529T265 548T213 554Z" />
<glyph unicode="S" glyph-name="S" d="M234 58Q297 58 330 82T363 154Q363 183 351 203T319 239T274 266T223 288Q193 300 166 315T117 350T83 397T70 461Q70 542 121 587T263 633Q288 633 312 630T358 621T395 609T422 595L396 526Q376 538 341 550T263 562Q218
562 185 540T152 472Q152 446 161 428T187 397T226 372T274 350Q312 334 343 318T397 280T432 228T445 155Q445 74 391 31T234 -13Q201 -13 172 -9T121 3T81 17T55 30L80 98Q100 87 139 73T234 58Z" />
<glyph unicode="T" glyph-name="T" d="M461 619V549H291V0H209V549H39V619H461Z" />
<glyph unicode="U" glyph-name="U" d="M250 -13Q195 -13 157 3T94 48T59 119T48 213V619H130V222Q130 175 138 144T162 93T200 66T250 58Q278 58 300 66T337 93T361 143T370 222V619H452V213Q452 161 441 119T406 48T344 3T250 -13Z" />
<glyph unicode="V" glyph-name="V" d="M102 619Q114 556 132 482T171 333T213 190T252 71Q268 122 289 191T332 334T371 483T402 619H488Q480 579 462 512T419 361T364 184T300 0H198Q167 90 138 183T83 359T40 511T13 619H102Z" />
<glyph unicode="W" glyph-name="W" d="M153 0H76Q67 61 61 134T49 288T39 452T32 619H112L123 102L213 384H285L377 102L388 619H468Q465 535 461 451T450 288T437 135T422 0H346L249 308L153 0Z" />
<glyph unicode="X" glyph-name="X" d="M384 0Q373 27 358 61T326 131T290 203T249 270Q214 214 179 142T114 0H25Q58 74 102 160T196 327L35 619H126L247 382L377 619H466L302 330Q350 252 395 168T475 0H384Z" />
<glyph unicode="Y" glyph-name="Y" d="M210 0V230Q149 330 100 425T13 619H105Q134 535 171 459T252 301Q298 386 333 462T398 619H488Q450 521 401 428T292 232V0H210Z" />
<glyph unicode="Z" glyph-name="Z" d="M447 555Q415 511 376 451T295 324T216 192T148 70H455V0H54V54Q85 117 122 183T199 314T277 438T352 549H69V619H447V555Z" />
<glyph unicode="[" glyph-name="bracketleft" d="M143 699H382V634H221V-100H382V-165H143V699Z" />
<glyph unicode="\" glyph-name="backslash" d="M70 699H153L430 -165H346L70 699Z" />
<glyph unicode="]" glyph-name="bracketright" d="M357 -165H118V-100H279V634H118V699H357V-165Z" />
<glyph unicode="^" glyph-name="asciicircum" d="M287 619L460 315L394 281L250 533L106 281L40 315L213 619H287Z" />
<glyph unicode="_" glyph-name="underscore" d="M8 -95H492V-165H8V-95Z" />
<glyph unicode="`" glyph-name="grave" d="M216 693L330 555L288 518L163 640L216 693Z" />
<glyph unicode="a" glyph-name="a" d="M255 59Q286 59 310 61T351 66V204Q335 209 313 212T265 215Q242 215 220 212T181 199T153 176T142 139Q142 94 172 77T255 59ZM247 475Q299 475 334 462T392 426T423 370T432 298V8Q403 3 356 -4T258 -11Q220 -11 184 -5T120
18T75 64T58 138Q58 178 75 205T120 250T183 274T255 282Q304 282 351 271V294Q351 315 347 334T330 370T296 395T240 405Q197 405 165 399T116 387L106 455Q123 463 162 469T247 475Z" />
<glyph unicode="b" glyph-name="b" d="M154 447Q169 456 197 465T261 475Q308 475 345 457T407 407T446 331T460 232Q460 176 444 131T399 55T329 6T238 -11Q183 -11 140 -3T71 13V679L154 693V447ZM154 72Q173 67 192 65T228 62Q297 62 336 104T375 232Q375 268
368 299T346 353T308 389T251 402Q222 402 195 391T154 366V72Z" />
<glyph unicode="c" glyph-name="c" d="M49 231Q49 294 69 340T124 416T205 460T305 475Q339 475 372 471T444 454L425 383Q392 395 365 398T309 402Q273 402 241 393T186 363T148 310T134 231Q134 187 147 156T183 104T240 73T313 63Q345 63 374 66T439 82L451
13Q416 0 380 -5T302 -11Q246 -11 200 4T120 50T68 125T49 231Z" />
<glyph unicode="d" glyph-name="d" d="M346 366Q332 379 305 390T249 402Q216 402 193 389T154 354T132 300T125 232Q125 151 163 107T262 63Q293 63 314 66T346 72V366ZM346 679L429 693V13Q402 5 360 -3T262 -11Q212 -11 171 6T101 54T56 131T40 232Q40 286
53 330T93 407T155 457T239 475Q276 475 304 466T346 447V679Z" />
<glyph unicode="e" glyph-name="e" d="M259 475Q353 475 404 417T455 239V210H123Q128 138 170 101T290 63Q334 63 365 70T412 85L423 15Q408 7 369 -2T281 -11Q221 -11 176 7T100 58T55 134T40 231Q40 293 59 339T109 415T179 460T259 475ZM371 277Q371 336 340
370T258 405Q229 405 206 394T165 365T138 324T125 277H371Z" />
<glyph unicode="f" glyph-name="f" d="M161 0V395H63V464H161V506Q161 560 176 595T215 653T272 684T341 693Q377 693 413 685T482 666L467 595Q445 606 412 614T345 622Q324 622 306 617T273 598T251 562T243 505V464H430V395H243V0H161Z" />
<glyph unicode="g" glyph-name="g" d="M346 43Q332 35 303 26T236 17Q195 17 160 30T98 71T56 141T40 242Q40 293 54 335T96 409T164 457T255 475Q315 475 357 467T429 450V36Q429 -72 375 -120T210 -168Q165 -168 129 -161T64 -144L79 -71Q105 -82 138 -89T212
-96Q284 -96 315 -67T346 27V43ZM346 391Q334 395 313 398T256 402Q192 402 159 358T125 241Q125 201 134 173T160 126T198 99T243 90Q274 90 301 99T346 120V391Z" />
<glyph unicode="h" glyph-name="h" d="M71 0V679L154 693V457Q174 465 199 469T248 474Q301 474 336 459T393 415T423 348T432 262V0H350V244Q350 330 326 365T240 401Q214 401 190 396T154 385V0H71Z" />
<glyph unicode="i" glyph-name="i" d="M209 536Q183 536 164 554T145 603Q145 634 164 652T209 670Q236 670 254 652T273 603Q273 572 255 554T209 536ZM188 395H54V464H270V179Q270 110 289 87T346 63Q375 63 399 70T438 85L450 15Q444 12 433 8T408 -1T376 -8T339
-11Q295 -11 266 1T220 37T195 96T188 179V395Z" />
<glyph unicode="j" glyph-name="j" d="M308 536Q282 536 263 554T244 603Q244 634 263 652T308 670Q335 670 353 652T372 603Q372 572 354 554T308 536ZM114 395V464H384V5Q384 -43 371 -76T335 -131T283 -161T220 -170Q182 -170 144 -162T71 -137L96 -67Q121
-79 151 -87T210 -96Q250 -96 276 -76T302 4V395H114Z" />
<glyph unicode="k" glyph-name="k" d="M238 255Q265 235 299 205T366 139T430 68T478 0H380Q361 31 333 63T274 126T211 181T154 223V0H71V679L154 693V270Q209 318 264 365T363 464H460Q417 413 357 359T238 255Z" />
<glyph unicode="l" glyph-name="l" d="M338 -11Q294 -11 265 1T219 37T195 96T188 179V618H54V688H270V179Q270 144 274 122T288 87T312 68T345 63Q374 63 399 70T438 85L450 15Q444 12 433 8T407 -1T375 -8T338 -11Z" />
<glyph unicode="m" glyph-name="m" d="M41 451Q101 474 154 474Q183 474 208 466T252 439Q295 474 344 474Q368 474 389 465T427 438T454 393T464 330V0H389V332Q389 368 373 387T333 406Q321 406 308 400T284 381Q290 358 290 330V179H215V331Q215 366 204 386T161
406Q141 406 116 397V0H41V451Z" />
<glyph unicode="n" glyph-name="n" d="M71 451Q116 462 160 468T244 474Q337 474 384 426T432 272V0H350V257Q350 302 342 330T320 373T285 395T240 401Q220 401 198 399T154 392V0H71V451Z" />
<glyph unicode="o" glyph-name="o" d="M460 232Q460 177 445 133T401 56T334 7T249 -11Q204 -11 166 6T99 56T56 132T40 232Q40 287 55 332T99 408T165 457T249 475Q295 475 334 458T401 409T444 332T460 232ZM375 232Q375 311 341 356T249 402Q192 402 159 357T125
232Q125 154 158 109T249 63Q307 63 341 108T375 232Z" />
<glyph unicode="p" glyph-name="p" d="M375 231Q375 312 337 356T238 401Q204 401 185 398T154 391V98Q168 85 195 74T251 63Q284 63 307 76T346 111T368 164T375 231ZM460 231Q460 178 447 134T408 57T345 7T261 -11Q224 -11 196 -2T154 18V-165H71V450Q98 458
140 466T238 474Q288 474 329 457T399 409T444 332T460 231Z" />
<glyph unicode="q" glyph-name="q" d="M125 231Q125 195 132 165T155 112T193 76T250 63Q279 63 306 74T347 98V391Q336 395 317 398T263 401Q202 401 164 357T125 231ZM40 231Q40 287 56 332T101 408T171 457T263 474Q317 474 358 466T429 449V-165H346V18Q331
8 303 -1T240 -11Q193 -11 156 7T94 57T54 133T40 231Z" />
<glyph unicode="r" glyph-name="r" d="M107 0V439Q209 474 313 474Q345 474 374 472T438 461L423 388Q391 397 367 399T313 402Q252 402 189 385V0H107Z" />
<glyph unicode="s" glyph-name="s" d="M351 116Q351 141 331 157T280 185T213 208T147 237T96 279T75 344Q75 400 120 437T263 475Q301 475 341 470T411 455L396 381Q388 385 374 389T342 397T304 403T264 405Q156 405 156 346Q156 325 176 311T228 284T295 260T362
229T413 186T434 121Q434 58 385 24T230 -11Q182 -11 142 -3T66 21L82 96Q117 80 155 70T238 59Q351 59 351 116Z" />
<glyph unicode="t" glyph-name="t" d="M243 464H439V395H243V179Q243 144 248 122T264 87T294 68T338 63Q374 63 396 69T438 85L450 15Q436 9 406 -1T330 -11Q278 -11 245 1T194 37T168 96T161 179V395H63V464H161V594L243 608V464Z" />
<glyph unicode="u" glyph-name="u" d="M429 13Q402 6 358 -2T253 -10Q200 -10 165 5T108 49T77 117T68 203V464H150V221Q150 135 175 99T261 63Q274 63 287 64T313 66T334 69T346 72V464H429V13Z" />
<glyph unicode="v" glyph-name="v" d="M470 464Q452 399 429 337T382 215T333 101T285 0H210Q163 94 115 214T30 464H120Q132 420 147 371T180 273T214 179T249 97Q266 133 284 178T321 273T355 371T384 464H470Z" />
<glyph unicode="w" glyph-name="w" d="M343 0Q328 34 317 62T296 118T275 176T250 246Q236 207 225 177T204 118T182 62T155 0H93Q65 123 47 243T19 464H97Q100 425 103 388T112 309T123 220T138 112Q154 152 165 182T185 239T202 294T221 357H283Q293 322 301
294T318 239T337 182T362 113Q371 168 378 215T390 303T399 384T406 464H481Q476 414 469 357T452 240T430 119T405 0H343Z" />
<glyph unicode="x" glyph-name="x" d="M380 0Q370 20 356 44T324 93T289 144T252 191Q234 168 214 143T176 93T141 43T114 0H29Q62 60 109 123T203 244L36 464H129L254 302L369 464H457L304 249Q349 190 394 126T472 0H380Z" />
<glyph unicode="y" glyph-name="y" d="M51 -87Q60 -92 76 -95T107 -98Q156 -98 183 -77T234 -6Q178 100 129 221T48 464H138Q148 424 161 378T192 282T228 184T270 89Q286 135 300 180T326 270T350 363T375 464H461Q429 334 390 212T305 -14Q288 -54 269 -83T226
-131T174 -159T108 -168Q89 -168 67 -163T36 -154L51 -87Z" />
<glyph unicode="z" glyph-name="z" d="M420 402Q402 381 371 344T305 261T234 165T172 70H427V0H73V55Q94 95 125 142T191 236T257 325T314 395H86V464H420V402Z" />
<glyph unicode="{" glyph-name="braceleft" d="M79 299H119Q136 299 149 306T170 326T183 354T188 386V547Q188 584 195 612T221 659T269 689T346 699H427V634H342Q300 634 283 615T265 546V408Q265 341 245 310T199 267Q225 255 245 222T265 126V-12Q265 -62
283 -81T343 -100H427V-165H346Q300 -165 270 -155T221 -126T196 -78T188 -13V148Q188 163 184 178T171 206T150 227T120 235H79V299Z" />
<glyph unicode="|" glyph-name="bar" d="M212 699H289V-165H212V699Z" />
<glyph unicode="}" glyph-name="braceright" d="M421 235H381Q364 235 352 228T331 208T318 180T313 148V-13Q313 -50 306 -78T280 -125T231 -155T154 -165H73V-100H158Q200 -100 217 -81T235 -12V126Q235 193 255 224T301 267Q275 279 255 312T235 408V546Q235
596 217 615T157 634H73V699H154Q200 699 230 689T279 660T305 612T313 547V386Q313 371 317 356T329 328T350 307T380 299H421V235Z" />
<glyph unicode="~" glyph-name="asciitilde" d="M463 314Q460 297 453 275T431 234T394 202T341 189Q314 189 292 199T248 223Q224 237 202 251T154 265Q140 265 130 259T114 242T102 220T94 196L37 212Q40 229 48 251T70 292T105 324T159 337Q186 337 208 327T252
303Q276 289 298 275T346 261Q360 261 370 267T386 284T398 306T406 330L463 314Z" />
<glyph unicode="&#xa0;" glyph-name="uni00A0" />
<glyph unicode="&#xa1;" glyph-name="exclamdown" d="M204 3Q204 40 205 71T209 130T214 185T221 243H278Q281 213 284 186T289 130T293 71T294 3V-165H204V3ZM249 465Q277 465 296 447T316 397Q316 366 297 348T249 330Q222 330 202 348T182 397Q182 428 202
446T249 465Z" />
<glyph unicode="&#xa2;" glyph-name="cent" d="M49 260Q49 313 64 353T106 423T170 469T252 492V619H326V495Q353 493 380 489T438 474L420 405Q387 417 360 420T305 424Q269 424 238 415T183 386T147 335T134 260Q134 177 183 138T313 98Q345 98 374 102T438
118L450 50Q420 39 390 34T326 27V-97H252V29Q160 42 105 100T49 260Z" />
<glyph unicode="&#xa3;" glyph-name="sterling" d="M210 333H380V266H210V259Q210 214 207 165T196 71H455V0H103Q113 64 120 126T128 253V266H45V333H128V407Q128 473 142 516T181 585T241 620T321 630Q360 630 386 624T438 606L418 535Q373 557 317 557Q293
557 274 550T240 526T218 481T210 409V333Z" />
<glyph unicode="&#xa4;" glyph-name="currency" d="M78 310Q78 365 103 405L38 471L89 522L155 455Q198 481 250 481Q301 481 344 455L411 522L463 471L396 405Q422 365 422 310Q422 281 416 258T396 215L463 149L411 97L344 163Q301 138 250 138Q196 138 155
163L89 97L38 149L103 214Q78 254 78 310ZM355 310Q355 361 324 391T250 421Q207 421 176 391T145 310Q145 259 176 229T250 199Q293 199 324 229T355 310Z" />
<glyph unicode="&#xa5;" glyph-name="yen" d="M210 0V95H65V158H210V249H65V313H187Q138 391 95 468T13 619H104Q136 549 174 480T253 346Q292 410 328 479T397 619H488Q448 544 405 468T313 313H436V249H292V158H436V95H292V0H210Z" />
<glyph unicode="&#xa6;" glyph-name="brokenbar" d="M212 177H289V-165H212V177ZM212 699H289V357H212V699Z" />
<glyph unicode="&#xa7;" glyph-name="section" d="M90 -1Q117 -12 149 -20T231 -29Q332 -29 332 33Q332 65 308 82T227 119Q194 130 166 142T115 172T81 215T68 276Q68 299 75 319T94 355T120 384T147 405Q125 423 112 446T99 499Q99 561 142 597T264 633Q310
633 349 625T412 608L393 539Q371 547 341 556T263 565Q228 565 203 551T178 506Q178 475 200 458T272 425Q305 414 333 402T384 371T418 329T431 268Q431 246 424 227T406 191T382 161T355 140Q381 121 397 96T413 38Q413 -29 366 -63T230 -97Q170 -97 132 -88T70
-67L90 -1ZM202 373Q176 356 160 334T144 284Q144 261 154 245T183 217T226 196T280 177L297 171Q323 188 339 210T355 260Q355 283 345 298T317 325T276 345T226 364L202 373Z" />
<glyph unicode="&#xa8;" glyph-name="dieresis" d="M165 550Q145 550 129 565T113 603Q113 626 129 640T165 655Q186 655 201 641T217 603Q217 580 202 565T165 550ZM335 550Q314 550 299 565T283 603Q283 626 298 640T335 655Q356 655 371 641T387 603Q387 580
372 565T335 550Z" />
<glyph unicode="&#xa9;" glyph-name="copyright" d="M408 231Q408 279 395 314T359 373T308 409T250 421Q220 421 192 409T142 374T106 315T92 233Q92 185 105 150T141 91T192 55T250 43Q280 43 308 55T359 90T394 149T408 231ZM327 120Q310 112 292 108T256 104Q227
104 206 114T170 142T149 182T142 229Q142 253 149 276T170 317T205 346T257 357Q273 357 290 355T325 344L305 290Q294 295 283 296T263 298Q231 298 218 278T204 233Q204 201 220 183T267 164Q276 164 287 166T310 173L327 120ZM34 231Q34 292 53 338T102 414T172
460T251 475Q291 475 330 460T399 415T447 339T466 233Q466 172 447 127T398 51T328 5T249 -11Q208 -11 170 4T101 50T53 125T34 231Z" />
<glyph unicode="&#xaa;" glyph-name="ordfeminine" d="M259 349Q279 349 298 350T322 353V442Q312 444 297 445T268 446Q253 446 238 444T209 437T188 423T180 398Q180 372 201 361T259 349ZM253 630Q326 630 355 596T384 506V306Q362 301 326 298T253 294Q194
294 156 318T118 398Q118 426 129 445T161 477T206 494T261 500Q274 500 288 499T322 496V498Q322 511 320 524T309 548T287 565T249 572Q228 572 204 570T162 561L153 617Q170 622 197 626T253 630Z" />
<glyph unicode="&#xab;" glyph-name="guillemotleft" d="M63 254L182 430L234 403L150 254L230 110L180 79L63 254ZM242 254L362 430L413 403L330 254L410 110L360 79L242 254Z" />
<glyph unicode="&#xac;" glyph-name="logicalnot" d="M450 347V43H379V277H41V347H450Z" />
<glyph unicode="&#xad;" glyph-name="uni00AD" d="M140 293H360V215H140V293Z" />
<glyph unicode="&#xae;" glyph-name="registered" d="M408 231Q408 279 395 314T359 373T308 409T250 421Q220 421 192 409T142 374T106 315T92 233Q92 185 105 150T141 91T192 55T250 43Q280 43 308 55T359 90T394 149T408 231ZM34 231Q34 292 53 338T102 414T172
460T251 475Q291 475 330 460T399 415T447 339T466 233Q466 172 447 127T398 51T328 5T249 -11Q208 -11 170 4T101 50T53 125T34 231ZM220 187V110H166V348Q180 352 197 354T233 357Q290 357 317 335T345 271Q345 223 305 200Q317 183 330 160T356 110H299Q286
134 276 152T256 187H220ZM220 237H238Q265 237 278 244T291 274Q291 294 275 300T242 307Q236 307 231 307T220 305V237Z" />
<glyph unicode="&#xaf;" glyph-name="overscore" d="M123 635H377V571H123V635Z" />
<glyph unicode="&#xb0;" glyph-name="degree" d="M375 569Q375 540 365 517T338 478T298 454T250 445Q225 445 203 453T163 478T135 517T125 569Q125 598 135 621T162 660T202 684T250 693Q275 693 297 685T337 660T365 621T375 569ZM315 569Q315 601 296 619T250
637Q223 637 204 619T185 569Q185 537 204 519T250 501Q277 501 296 519T315 569Z" />
<glyph unicode="&#xb1;" glyph-name="plusminus" d="M46 374H214V554H286V374H455V304H286V124H214V304H46V374ZM46 71H455V0H46V71Z" />
<glyph unicode="&#xb2;" glyph-name="twosuperior" d="M362 534Q362 515 354 498T331 463T297 427T257 391Q242 378 227 364T210 338H376V281H138Q135 311 142 333T162 375T192 409T228 441Q258 467 277 489T296 532Q296 577 241 577Q226 577 213 573T188 564T169
552T156 542L124 586Q140 602 170 617T241 633Q303 633 332 608T362 534Z" />
<glyph unicode="&#xb3;" glyph-name="threesuperior" d="M226 330Q274 330 291 344T309 382Q309 397 301 407T280 425T248 435T211 438H194V489H216Q229 489 242 491T267 499T285 514T293 537Q293 559 277 568T234 578Q210 578 190 570T154 554L131 603Q145 613
175 623T236 633Q301 633 328 607T355 539Q355 496 306 468Q338 458 356 436T374 381Q374 336 340 305T227 274Q199 274 171 280T126 295L139 348Q159 340 181 335T226 330Z" />
<glyph unicode="&#xb4;" glyph-name="acute" d="M338 640L212 518L170 555L284 693L338 640Z" />
<glyph unicode="&#xb5;" glyph-name="mu" d="M429 13Q402 6 358 -2T253 -11Q212 -11 190 -2T150 22Q153 -1 153 -24T154 -73V-165H71V464H154V221Q154 178 159 148T177 99T210 72T261 63Q274 63 287 64T313 66T334 68T346 71V464H429V13Z" />
<glyph unicode="&#xb6;" glyph-name="paragraph" d="M377 553Q331 559 290 555V-165H221V234Q128 244 78 290T28 428Q28 477 46 513T100 575T184 612T296 625Q331 625 371 622T447 609V-165H377V553Z" />
<glyph unicode="&#xb7;" glyph-name="middot" d="M324 260Q324 230 304 207T251 184Q217 184 197 207T177 260Q177 291 197 314T251 337Q284 337 304 314T324 260Z" />
<glyph unicode="&#xb8;" glyph-name="cedilla" d="M272 -33Q298 -45 309 -60T321 -104Q321 -114 317 -125T302 -146T274 -162T230 -169Q207 -169 187 -166T156 -157L166 -106Q177 -110 189 -112T216 -115Q248 -115 248 -95Q248 -75 211 -64L204 -62Q207 -53 212
-41T223 -17T234 4T241 18H298Q293 7 285 -7T272 -33Z" />
<glyph unicode="&#xb9;" glyph-name="onesuperior" d="M231 338V544Q211 531 186 521T143 506L126 555Q160 566 193 584T250 626H294V338H383V281H133V338H231Z" />
<glyph unicode="&#xba;" glyph-name="ordmasculine" d="M411 463Q411 424 399 393T366 340T315 305T250 293Q215 293 186 305T135 339T101 393T89 463Q89 502 101 533T134 586T185 620T250 632Q285 632 314 620T365 586T399 533T411 463ZM345 463Q345 514 321
543T250 572Q205 572 180 543T155 463Q155 413 179 383T250 353Q296 353 320 383T345 463Z" />
<glyph unicode="&#xbb;" glyph-name="guillemotright" d="M437 255L317 79L265 106L349 255L269 399L319 430L437 255ZM256 255L137 79L85 106L169 255L88 399L138 430L256 255Z" />
<glyph unicode="&#xbc;" glyph-name="onequarter" d="M18 561Q52 575 76 589T124 626H164V376H105V544Q85 533 69 526T35 513L18 561ZM441 250V98H477V48H441V0H382V48H253V89Q279 135 303 172T358 250H441ZM382 207Q361 180 344 152T312 98H382V207ZM95 -4H25L404
615H474L95 -4Z" />
<glyph unicode="&#xbd;" glyph-name="onehalf" d="M482 50V0H285Q284 3 284 11T284 23Q284 55 304 72T350 106L372 121Q396 135 405 147T414 174Q414 191 403 199T375 207Q360 207 344 201T316 181L284 221Q301 240 327 249T379 259Q426 259 449 234T473 177Q473
151 460 133T428 100T389 74T356 50H482ZM24 561Q59 575 82 589T130 626H171V376H112V544Q92 533 76 526T41 513L24 561ZM87 0H17L396 619H466L87 0Z" />
<glyph unicode="&#xbe;" glyph-name="threequarters" d="M441 250V98H477V48H441V0H382V48H253V89Q279 135 303 172T358 250H441ZM36 430Q47 425 63 421T100 417Q122 417 138 423T155 447Q155 468 136 474T79 480H67V530H89Q115 530 129 535T143 559Q143 574 131
578T104 583Q90 583 72 578T42 568L23 611Q38 620 62 626T113 633Q130 633 146 630T174 619T194 599T202 568Q202 553 195 538T171 512Q214 492 214 447Q214 433 208 419T189 393T155 374T104 367Q58 367 21 381L36 430ZM382 207Q361 180 344 152T312 98H382V207ZM100
0H30L409 619H479L100 0Z" />
<glyph unicode="&#xbf;" glyph-name="questiondown" d="M317 243L318 223Q318 194 308 172T282 131T248 96T214 62T188 26T177 -16Q177 -54 201 -77T271 -100Q330 -100 387 -70L410 -134Q380 -150 342 -160T261 -170Q210 -170 178 -156T126 -121T100 -73T92 -23Q92
7 103 30T132 72T169 110T206 148T234 190T246 243H317ZM274 465Q302 465 321 447T341 397Q341 366 322 348T274 330Q247 330 227 348T207 397Q207 428 227 446T274 465Z" />
<glyph unicode="&#xc0;" glyph-name="Agrave" d="M404 0L367 162H129L93 0H9Q26 66 47 143T92 301T144 463T201 619H303Q332 545 358 464T408 301T453 143T491 0H404ZM249 547Q226 484 199 401T148 230H348Q325 320 299 403T249 547ZM220 830L334 692L292 655L167
777L220 830Z" />
<glyph unicode="&#xc1;" glyph-name="Aacute" d="M404 0L367 162H129L93 0H9Q26 66 47 143T92 301T144 463T201 619H303Q332 545 358 464T408 301T453 143T491 0H404ZM249 547Q226 484 199 401T148 230H348Q325 320 299 403T249 547ZM335 777L209 655L167 692L281
830L335 777Z" />
<glyph unicode="&#xc2;" glyph-name="Acircumflex" d="M404 0L367 162H129L93 0H9Q26 66 47 143T92 301T144 463T201 619H303Q332 545 358 464T408 301T453 143T491 0H404ZM249 547Q226 484 199 401T148 230H348Q325 320 299 403T249 547ZM250 826L371 698L337
659L250 737L163 659L129 698L250 826Z" />
<glyph unicode="&#xc3;" glyph-name="Atilde" d="M404 0L367 162H129L93 0H9Q26 66 47 143T92 301T144 463T201 619H303Q332 545 358 464T408 301T453 143T491 0H404ZM249 547Q226 484 199 401T148 230H348Q325 320 299 403T249 547ZM388 774Q385 763 378 749T359
723T333 703T299 694Q286 694 269 701T240 713Q214 727 198 727Q185 727 173 714T155 687L112 710Q115 721 122 735T141 761T167 781T200 790Q213 790 230 783T259 770Q285 756 301 756Q314 756 326 769T345 796L388 774Z" />
<glyph unicode="&#xc4;" glyph-name="Adieresis" d="M404 0L367 162H129L93 0H9Q26 66 47 143T92 301T144 463T201 619H303Q332 545 358 464T408 301T453 143T491 0H404ZM249 547Q226 484 199 401T148 230H348Q325 320 299 403T249 547ZM165 689Q145 689 129 704T113
742Q113 765 129 779T165 794Q186 794 201 780T217 742Q217 719 202 704T165 689ZM335 689Q314 689 299 704T283 742Q283 765 298 779T335 794Q356 794 371 780T387 742Q387 719 372 704T335 689Z" />
<glyph unicode="&#xc5;" glyph-name="Aring" d="M404 0L367 162H129L93 0H9Q25 63 46 140T92 297T141 452T193 586Q179 598 169 616T159 658Q159 700 186 724T251 749Q288 749 315 725T343 658Q343 636 334 617T310 587Q335 528 360 453T409 297T453 140T491 0H404ZM249
547Q226 484 199 401T148 230H348Q325 320 299 403T249 547ZM298 658Q298 681 285 694T251 707Q231 707 218 694T204 658Q204 635 217 622T251 609Q271 609 284 622T298 658Z" />
<glyph unicode="&#xc6;" glyph-name="AE" d="M263 0V162H145L108 0H27Q72 174 118 326T219 619H469V549H341V359H446V289H341V70H482V0H263ZM263 566Q235 478 210 391T163 230H263V566Z" />
<glyph unicode="&#xc7;" glyph-name="Ccedilla" d="M317 -33Q343 -45 354 -60T366 -104Q366 -114 362 -125T347 -146T319 -162T275 -169Q252 -169 232 -166T201 -157L211 -106Q222 -110 234 -112T261 -115Q293 -115 293 -95Q293 -75 256 -64L249 -62Q253 -51 259
-38T271 -11Q222 -6 181 15T109 77T63 175T46 310Q46 389 66 449T122 550T204 612T304 633Q341 633 379 623T455 590L431 522Q365 560 307 560Q266 560 234 543T178 492T143 413T130 310Q130 246 143 199T181 121T238 75T311 60Q340 60 372 67T438 94L460 26Q402
-7 328 -12L317 -33Z" />
<glyph unicode="&#xc8;" glyph-name="Egrave" d="M91 0V619H438V549H173V359H405V289H173V70H460V0H91ZM230 830L344 692L302 655L177 777L230 830Z" />
<glyph unicode="&#xc9;" glyph-name="Eacute" d="M91 0V619H438V549H173V359H405V289H173V70H460V0H91ZM364 777L238 655L196 692L310 830L364 777Z" />
<glyph unicode="&#xca;" glyph-name="Ecircumflex" d="M91 0V619H438V549H173V359H405V289H173V70H460V0H91ZM268 826L389 698L355 659L268 737L181 659L147 698L268 826Z" />
<glyph unicode="&#xcb;" glyph-name="Edieresis" d="M91 0V619H438V549H173V359H405V289H173V70H460V0H91ZM183 689Q163 689 147 704T131 742Q131 765 147 779T183 794Q204 794 219 780T235 742Q235 719 220 704T183 689ZM353 689Q332 689 317 704T301 742Q301
765 316 779T353 794Q374 794 389 780T405 742Q405 719 390 704T353 689Z" />
<glyph unicode="&#xcc;" glyph-name="Igrave" d="M291 549V70H411V0H89V70H209V549H89V619H411V549H291ZM221 830L335 692L293 655L168 777L221 830Z" />
<glyph unicode="&#xcd;" glyph-name="Iacute" d="M291 549V70H411V0H89V70H209V549H89V619H411V549H291ZM348 777L222 655L180 692L294 830L348 777Z" />
<glyph unicode="&#xce;" glyph-name="Icircumflex" d="M291 549V70H411V0H89V70H209V549H89V619H411V549H291ZM250 826L371 698L337 659L250 737L163 659L129 698L250 826Z" />
<glyph unicode="&#xcf;" glyph-name="Idieresis" d="M291 549V70H411V0H89V70H209V549H89V619H411V549H291ZM165 689Q145 689 129 704T113 742Q113 765 129 779T165 794Q186 794 201 780T217 742Q217 719 202 704T165 689ZM335 689Q314 689 299 704T283 742Q283
765 298 779T335 794Q356 794 371 780T387 742Q387 719 372 704T335 689Z" />
<glyph unicode="&#xd0;" glyph-name="Eth" d="M460 310Q460 224 440 164T384 67T299 12T193 -5Q132 -5 71 10V291H18V355H71V610Q132 625 193 625Q250 625 299 608T383 553T439 455T460 310ZM154 69Q165 67 177 67T200 66Q242 66 274 79T329 122T364 198T376 310Q376
436 331 495T197 554Q172 554 154 551V355H252V291H154V69Z" />
<glyph unicode="&#xd1;" glyph-name="Ntilde" d="M361 0Q327 83 297 150T240 277T185 388T130 494V0H55V619H139Q180 544 210 488T266 380T316 272T370 143V619H445V0H361ZM388 774Q385 763 378 749T359 723T333 703T299 694Q286 694 269 701T240 713Q214 727
198 727Q185 727 173 714T155 687L112 710Q115 721 122 735T141 761T167 781T200 790Q213 790 230 783T259 770Q285 756 301 756Q314 756 326 769T345 796L388 774Z" />
<glyph unicode="&#xd2;" glyph-name="Ograve" d="M29 310Q29 393 45 454T90 554T159 613T250 633Q299 633 340 614T410 555T455 454T472 310Q472 227 456 166T410 65T340 6T250 -13Q200 -13 160 6T90 65T45 166T29 310ZM113 310Q113 188 146 123T248 58Q318 58
353 123T388 310Q388 432 353 497T248 562Q179 562 146 497T113 310ZM221 830L335 692L293 655L168 777L221 830Z" />
<glyph unicode="&#xd3;" glyph-name="Oacute" d="M29 310Q29 393 45 454T90 554T159 613T250 633Q299 633 340 614T410 555T455 454T472 310Q472 227 456 166T410 65T340 6T250 -13Q200 -13 160 6T90 65T45 166T29 310ZM113 310Q113 188 146 123T248 58Q318 58
353 123T388 310Q388 432 353 497T248 562Q179 562 146 497T113 310ZM347 777L221 655L179 692L293 830L347 777Z" />
<glyph unicode="&#xd4;" glyph-name="Ocircumflex" d="M29 310Q29 393 45 454T90 554T159 613T250 633Q299 633 340 614T410 555T455 454T472 310Q472 227 456 166T410 65T340 6T250 -13Q200 -13 160 6T90 65T45 166T29 310ZM113 310Q113 188 146 123T248 58Q318
58 353 123T388 310Q388 432 353 497T248 562Q179 562 146 497T113 310ZM250 826L371 698L337 659L250 737L163 659L129 698L250 826Z" />
<glyph unicode="&#xd5;" glyph-name="Otilde" d="M29 310Q29 393 45 454T90 554T159 613T250 633Q299 633 340 614T410 555T455 454T472 310Q472 227 456 166T410 65T340 6T250 -13Q200 -13 160 6T90 65T45 166T29 310ZM113 310Q113 188 146 123T248 58Q318 58
353 123T388 310Q388 432 353 497T248 562Q179 562 146 497T113 310ZM388 774Q385 763 378 749T359 723T333 703T299 694Q286 694 269 701T240 713Q214 727 198 727Q185 727 173 714T155 687L112 710Q115 721 122 735T141 761T167 781T200 790Q213 790 230 783T259
770Q285 756 301 756Q314 756 326 769T345 796L388 774Z" />
<glyph unicode="&#xd6;" glyph-name="Odieresis" d="M29 310Q29 393 45 454T90 554T159 613T250 633Q299 633 340 614T410 555T455 454T472 310Q472 227 456 166T410 65T340 6T250 -13Q200 -13 160 6T90 65T45 166T29 310ZM113 310Q113 188 146 123T248 58Q318
58 353 123T388 310Q388 432 353 497T248 562Q179 562 146 497T113 310ZM165 689Q145 689 129 704T113 742Q113 765 129 779T165 794Q186 794 201 780T217 742Q217 719 202 704T165 689ZM335 689Q314 689 299 704T283 742Q283 765 298 779T335 794Q356 794 371
780T387 742Q387 719 372 704T335 689Z" />
<glyph unicode="&#xd7;" glyph-name="multiply" d="M380 82L250 213L120 82L70 132L200 263L70 393L120 443L250 313L380 443L430 393L300 263L430 132L380 82Z" />
<glyph unicode="&#xd8;" glyph-name="Oslash" d="M29 310Q29 393 45 454T90 554T159 613T250 633Q306 633 353 606L374 651L436 620L406 559Q437 518 454 457T472 310Q472 227 456 166T410 65T340 6T250 -13Q188 -13 142 15L119 -33L58 -2L90 64Q60 104 45 164T29
310ZM388 310Q388 360 383 402T365 475L174 81Q189 68 208 63T248 58Q318 58 353 123T388 310ZM113 310Q113 262 116 221T130 148L320 540Q305 552 287 557T248 562Q179 562 146 497T113 310Z" />
<glyph unicode="&#xd9;" glyph-name="Ugrave" d="M250 -13Q195 -13 157 3T94 48T59 119T48 213V619H130V222Q130 175 138 144T162 93T200 66T250 58Q278 58 300 66T337 93T361 143T370 222V619H452V213Q452 161 441 119T406 48T344 3T250 -13ZM216 830L330 692L288
655L163 777L216 830Z" />
<glyph unicode="&#xda;" glyph-name="Uacute" d="M250 -13Q195 -13 157 3T94 48T59 119T48 213V619H130V222Q130 175 138 144T162 93T200 66T250 58Q278 58 300 66T337 93T361 143T370 222V619H452V213Q452 161 441 119T406 48T344 3T250 -13ZM347 777L221 655L179
692L293 830L347 777Z" />
<glyph unicode="&#xdb;" glyph-name="Ucircumflex" d="M250 -13Q195 -13 157 3T94 48T59 119T48 213V619H130V222Q130 175 138 144T162 93T200 66T250 58Q278 58 300 66T337 93T361 143T370 222V619H452V213Q452 161 441 119T406 48T344 3T250 -13ZM250 826L371
698L337 659L250 737L163 659L129 698L250 826Z" />
<glyph unicode="&#xdc;" glyph-name="Udieresis" d="M250 -13Q195 -13 157 3T94 48T59 119T48 213V619H130V222Q130 175 138 144T162 93T200 66T250 58Q278 58 300 66T337 93T361 143T370 222V619H452V213Q452 161 441 119T406 48T344 3T250 -13ZM165 689Q145
689 129 704T113 742Q113 765 129 779T165 794Q186 794 201 780T217 742Q217 719 202 704T165 689ZM335 689Q314 689 299 704T283 742Q283 765 298 779T335 794Q356 794 371 780T387 742Q387 719 372 704T335 689Z" />
<glyph unicode="&#xdd;" glyph-name="Yacute" d="M210 0V230Q149 330 100 425T13 619H105Q134 535 171 459T252 301Q298 386 333 462T398 619H488Q450 521 401 428T292 232V0H210ZM347 777L221 655L179 692L293 830L347 777Z" />
<glyph unicode="&#xde;" glyph-name="Thorn" d="M73 619H155V516Q186 519 216 519Q331 519 393 474T455 340Q455 281 438 241T388 176T311 139T210 128H155V0H73V619ZM229 447Q204 447 187 447T155 446V197H213Q248 197 277 203T327 226T359 268T371 336Q371 369
359 390T326 424T281 442T229 447Z" />
<glyph unicode="&#xdf;" glyph-name="germandbls" d="M400 550Q400 526 393 506T374 470T349 439T323 412Q309 396 303 386T296 360Q296 340 309 327T343 303T386 279T429 250T462 207T476 142Q476 66 435 28T307 -11Q291 -11 275 -8T244 -1T219 7T203 14L217
87Q222 84 232 80T255 71T283 63T313 60Q354 60 373 80T392 138Q392 165 379 182T345 213T301 238T257 264T224 298T210 348Q210 384 225 408T263 455Q284 475 300 495T317 538Q317 576 299 598T241 621Q199 621 177 587T154 493V0H71V495Q71 539 82 575T115 638T166
678T235 693Q281 693 313 682T364 650T391 604T400 550Z" />
<glyph unicode="&#xe0;" glyph-name="agrave" d="M255 59Q286 59 310 61T351 66V204Q335 209 313 212T265 215Q242 215 220 212T181 199T153 176T142 139Q142 94 172 77T255 59ZM247 475Q299 475 334 462T392 426T423 370T432 298V8Q403 3 356 -4T258 -11Q220
-11 184 -5T120 18T75 64T58 138Q58 178 75 205T120 250T183 274T255 282Q304 282 351 271V294Q351 315 347 334T330 370T296 395T240 405Q197 405 165 399T116 387L106 455Q123 463 162 469T247 475ZM216 693L330 555L288 518L163 640L216 693Z" />
<glyph unicode="&#xe1;" glyph-name="aacute" d="M255 59Q286 59 310 61T351 66V204Q335 209 313 212T265 215Q242 215 220 212T181 199T153 176T142 139Q142 94 172 77T255 59ZM247 475Q299 475 334 462T392 426T423 370T432 298V8Q403 3 356 -4T258 -11Q220
-11 184 -5T120 18T75 64T58 138Q58 178 75 205T120 250T183 274T255 282Q304 282 351 271V294Q351 315 347 334T330 370T296 395T240 405Q197 405 165 399T116 387L106 455Q123 463 162 469T247 475ZM356 640L230 518L188 555L302 693L356 640Z" />
<glyph unicode="&#xe2;" glyph-name="acircumflex" d="M255 59Q286 59 310 61T351 66V204Q335 209 313 212T265 215Q242 215 220 212T181 199T153 176T142 139Q142 94 172 77T255 59ZM247 475Q299 475 334 462T392 426T423 370T432 298V8Q403 3 356 -4T258 -11Q220
-11 184 -5T120 18T75 64T58 138Q58 178 75 205T120 250T183 274T255 282Q304 282 351 271V294Q351 315 347 334T330 370T296 395T240 405Q197 405 165 399T116 387L106 455Q123 463 162 469T247 475ZM259 687L380 559L346 520L259 598L172 520L138 559L259 687Z"
/>
<glyph unicode="&#xe3;" glyph-name="atilde" d="M255 59Q286 59 310 61T351 66V204Q335 209 313 212T265 215Q242 215 220 212T181 199T153 176T142 139Q142 94 172 77T255 59ZM247 475Q299 475 334 462T392 426T423 370T432 298V8Q403 3 356 -4T258 -11Q220
-11 184 -5T120 18T75 64T58 138Q58 178 75 205T120 250T183 274T255 282Q304 282 351 271V294Q351 315 347 334T330 370T296 395T240 405Q197 405 165 399T116 387L106 455Q123 463 162 469T247 475ZM388 635Q385 624 378 610T359 584T333 564T299 555Q286 555
269 562T240 574Q214 588 198 588Q185 588 173 575T155 548L112 571Q115 582 122 596T141 622T167 642T200 651Q213 651 230 644T259 631Q285 617 301 617Q314 617 326 630T345 657L388 635Z" />
<glyph unicode="&#xe4;" glyph-name="adieresis" d="M255 59Q286 59 310 61T351 66V204Q335 209 313 212T265 215Q242 215 220 212T181 199T153 176T142 139Q142 94 172 77T255 59ZM247 475Q299 475 334 462T392 426T423 370T432 298V8Q403 3 356 -4T258 -11Q220
-11 184 -5T120 18T75 64T58 138Q58 178 75 205T120 250T183 274T255 282Q304 282 351 271V294Q351 315 347 334T330 370T296 395T240 405Q197 405 165 399T116 387L106 455Q123 463 162 469T247 475ZM165 550Q145 550 129 565T113 603Q113 626 129 640T165 655Q186
655 201 641T217 603Q217 580 202 565T165 550ZM335 550Q314 550 299 565T283 603Q283 626 298 640T335 655Q356 655 371 641T387 603Q387 580 372 565T335 550Z" />
<glyph unicode="&#xe5;" glyph-name="aring" d="M255 59Q286 59 310 61T351 66V204Q335 209 313 212T265 215Q242 215 220 212T181 199T153 176T142 139Q142 94 172 77T255 59ZM247 475Q299 475 334 462T392 426T423 370T432 298V8Q403 3 356 -4T258 -11Q220 -11
184 -5T120 18T75 64T58 138Q58 178 75 205T120 250T183 274T255 282Q304 282 351 271V294Q351 315 347 334T330 370T296 395T240 405Q197 405 165 399T116 387L106 455Q123 463 162 469T247 475ZM342 605Q342 562 315 538T250 513Q213 513 186 537T158 605Q158
647 185 671T250 696Q287 696 314 672T342 605ZM297 605Q297 628 284 641T250 654Q230 654 217 641T203 605Q203 581 216 568T250 555Q270 555 283 568T297 605Z" />
<glyph unicode="&#xe6;" glyph-name="ae" d="M150 475Q194 475 222 458T265 411Q281 441 302 458T349 475Q478 475 478 239Q478 234 478 226T477 210H292Q292 138 318 101T385 63Q417 63 454 85L465 15Q461 11 452 7T430 -2T404 -8T377 -11Q344 -11 319 0T274
32Q247 9 219 -1T162 -11Q129 -11 105 1T64 34T39 79T31 132Q31 163 39 190T65 238T106 270T164 282Q176 282 190 280T217 271V294Q217 341 199 373T143 405Q121 405 100 399T71 387L62 455Q71 463 97 469T150 475ZM172 215Q139 215 123 194T106 137Q106 107 121
83T169 59Q186 59 205 67T239 88Q228 113 222 142T214 204Q194 215 172 215ZM403 277Q403 314 399 338T387 378T370 399T353 405Q330 405 312 376T294 277H403Z" />
<glyph unicode="&#xe7;" glyph-name="ccedilla" d="M294 -33Q320 -45 331 -60T343 -104Q343 -114 339 -125T324 -146T296 -162T252 -169Q229 -169 209 -166T178 -157L188 -106Q199 -110 211 -112T238 -115Q270 -115 270 -95Q270 -75 233 -64L226 -62Q230 -51 236
-36T250 -7Q159 6 104 65T49 231Q49 294 69 340T124 416T205 460T305 475Q339 475 372 471T444 454L425 383Q392 395 365 398T309 402Q273 402 241 393T186 363T148 310T134 231Q134 187 147 156T183 104T240 73T313 63Q345 63 374 66T439 82L451 13Q417 1 382
-4T305 -11L294 -33Z" />
<glyph unicode="&#xe8;" glyph-name="egrave" d="M259 475Q353 475 404 417T455 239V210H123Q128 138 170 101T290 63Q334 63 365 70T412 85L423 15Q408 7 369 -2T281 -11Q221 -11 176 7T100 58T55 134T40 231Q40 293 59 339T109 415T179 460T259 475ZM371 277Q371
336 340 370T258 405Q229 405 206 394T165 365T138 324T125 277H371ZM216 693L330 555L288 518L163 640L216 693Z" />
<glyph unicode="&#xe9;" glyph-name="eacute" d="M259 475Q353 475 404 417T455 239V210H123Q128 138 170 101T290 63Q334 63 365 70T412 85L423 15Q408 7 369 -2T281 -11Q221 -11 176 7T100 58T55 134T40 231Q40 293 59 339T109 415T179 460T259 475ZM371 277Q371
336 340 370T258 405Q229 405 206 394T165 365T138 324T125 277H371ZM356 640L230 518L188 555L302 693L356 640Z" />
<glyph unicode="&#xea;" glyph-name="ecircumflex" d="M259 475Q353 475 404 417T455 239V210H123Q128 138 170 101T290 63Q334 63 365 70T412 85L423 15Q408 7 369 -2T281 -11Q221 -11 176 7T100 58T55 134T40 231Q40 293 59 339T109 415T179 460T259 475ZM371
277Q371 336 340 370T258 405Q229 405 206 394T165 365T138 324T125 277H371ZM263 687L384 559L350 520L263 598L176 520L142 559L263 687Z" />
<glyph unicode="&#xeb;" glyph-name="edieresis" d="M259 475Q353 475 404 417T455 239V210H123Q128 138 170 101T290 63Q334 63 365 70T412 85L423 15Q408 7 369 -2T281 -11Q221 -11 176 7T100 58T55 134T40 231Q40 293 59 339T109 415T179 460T259 475ZM371
277Q371 336 340 370T258 405Q229 405 206 394T165 365T138 324T125 277H371ZM178 550Q158 550 142 565T126 603Q126 626 142 640T178 655Q199 655 214 641T230 603Q230 580 215 565T178 550ZM348 550Q327 550 312 565T296 603Q296 626 311 640T348 655Q369 655
384 641T400 603Q400 580 385 565T348 550Z" />
<glyph unicode="&#xec;" glyph-name="igrave" d="M188 395H54V464H270V179Q270 110 289 87T346 63Q375 63 399 70T438 85L450 15Q444 12 433 8T408 -1T376 -8T339 -11Q295 -11 266 1T220 37T195 96T188 179V395ZM437 693L551 555L509 518L384 640L437 693Z" />
<glyph unicode="&#xed;" glyph-name="iacute" d="M188 395H54V464H270V179Q270 110 289 87T346 63Q375 63 399 70T438 85L450 15Q444 12 433 8T408 -1T376 -8T339 -11Q295 -11 266 1T220 37T195 96T188 179V395ZM568 640L442 518L400 555L514 693L568 640Z" />
<glyph unicode="&#xee;" glyph-name="icircumflex" d="M188 395H54V464H270V179Q270 110 289 87T346 63Q375 63 399 70T438 85L450 15Q444 12 433 8T408 -1T376 -8T339 -11Q295 -11 266 1T220 37T195 96T188 179V395ZM471 687L592 559L558 520L471 598L384 520L350
559L471 687Z" />
<glyph unicode="&#xef;" glyph-name="idieresis" d="M188 395H54V464H270V179Q270 110 289 87T346 63Q375 63 399 70T438 85L450 15Q444 12 433 8T408 -1T376 -8T339 -11Q295 -11 266 1T220 37T195 96T188 179V395ZM386 550Q366 550 350 565T334 603Q334 626 350
640T386 655Q407 655 422 641T438 603Q438 580 423 565T386 550ZM556 550Q535 550 520 565T504 603Q504 626 519 640T556 655Q577 655 592 641T608 603Q608 580 593 565T556 550Z" />
<glyph unicode="&#xf0;" glyph-name="eth" d="M460 587L364 554Q393 505 412 435T432 274Q432 225 424 175T393 83T333 15T236 -11Q191 -11 157 8T101 58T67 127T55 204Q55 318 103 378T234 438Q272 438 301 424T344 396Q339 432 325 465T291 529L184 492L163
548L255 580Q240 597 225 612T186 645L232 693Q252 680 277 659T329 605L439 643L460 587ZM351 271Q351 293 351 302T350 319Q325 346 297 357T245 368Q189 368 163 323T137 207Q137 178 143 151T162 104T193 71T236 59Q270 59 292 77T328 126T346 194T351 271Z"
/>
<glyph unicode="&#xf1;" glyph-name="ntilde" d="M71 451Q116 462 160 468T244 474Q337 474 384 426T432 272V0H350V257Q350 302 342 330T320 373T285 395T240 401Q220 401 198 399T154 392V0H71V451ZM388 635Q385 624 378 610T359 584T333 564T299 555Q286 555
269 562T240 574Q214 588 198 588Q185 588 173 575T155 548L112 571Q115 582 122 596T141 622T167 642T200 651Q213 651 230 644T259 631Q285 617 301 617Q314 617 326 630T345 657L388 635Z" />
<glyph unicode="&#xf2;" glyph-name="ograve" d="M460 232Q460 177 445 133T401 56T334 7T249 -11Q204 -11 166 6T99 56T56 132T40 232Q40 287 55 332T99 408T165 457T249 475Q295 475 334 458T401 409T444 332T460 232ZM375 232Q375 311 341 356T249 402Q192
402 159 357T125 232Q125 154 158 109T249 63Q307 63 341 108T375 232ZM225 693L339 555L297 518L172 640L225 693Z" />
<glyph unicode="&#xf3;" glyph-name="oacute" d="M460 232Q460 177 445 133T401 56T334 7T249 -11Q204 -11 166 6T99 56T56 132T40 232Q40 287 55 332T99 408T165 457T249 475Q295 475 334 458T401 409T444 332T460 232ZM375 232Q375 311 341 356T249 402Q192
402 159 357T125 232Q125 154 158 109T249 63Q307 63 341 108T375 232ZM347 640L221 518L179 555L293 693L347 640Z" />
<glyph unicode="&#xf4;" glyph-name="ocircumflex" d="M460 232Q460 177 445 133T401 56T334 7T249 -11Q204 -11 166 6T99 56T56 132T40 232Q40 287 55 332T99 408T165 457T249 475Q295 475 334 458T401 409T444 332T460 232ZM375 232Q375 311 341 356T249 402Q192
402 159 357T125 232Q125 154 158 109T249 63Q307 63 341 108T375 232ZM250 687L371 559L337 520L250 598L163 520L129 559L250 687Z" />
<glyph unicode="&#xf5;" glyph-name="otilde" d="M460 232Q460 177 445 133T401 56T334 7T249 -11Q204 -11 166 6T99 56T56 132T40 232Q40 287 55 332T99 408T165 457T249 475Q295 475 334 458T401 409T444 332T460 232ZM375 232Q375 311 341 356T249 402Q192
402 159 357T125 232Q125 154 158 109T249 63Q307 63 341 108T375 232ZM388 635Q385 624 378 610T359 584T333 564T299 555Q286 555 269 562T240 574Q214 588 198 588Q185 588 173 575T155 548L112 571Q115 582 122 596T141 622T167 642T200 651Q213 651 230 644T259
631Q285 617 301 617Q314 617 326 630T345 657L388 635Z" />
<glyph unicode="&#xf6;" glyph-name="odieresis" d="M460 232Q460 177 445 133T401 56T334 7T249 -11Q204 -11 166 6T99 56T56 132T40 232Q40 287 55 332T99 408T165 457T249 475Q295 475 334 458T401 409T444 332T460 232ZM375 232Q375 311 341 356T249 402Q192
402 159 357T125 232Q125 154 158 109T249 63Q307 63 341 108T375 232ZM165 550Q145 550 129 565T113 603Q113 626 129 640T165 655Q186 655 201 641T217 603Q217 580 202 565T165 550ZM335 550Q314 550 299 565T283 603Q283 626 298 640T335 655Q356 655 371 641T387
603Q387 580 372 565T335 550Z" />
<glyph unicode="&#xf7;" glyph-name="divide" d="M46 298H455V228H46V298ZM308 96Q308 72 292 54T250 36Q223 36 207 54T190 96Q190 121 206 139T250 158Q276 158 292 140T308 96ZM308 428Q308 404 292 386T250 367Q223 367 207 385T190 428Q190 453 206 471T250
489Q276 489 292 471T308 428Z" />
<glyph unicode="&#xf8;" glyph-name="oslash" d="M45 4L90 66Q67 98 54 139T40 232Q40 287 55 332T99 408T165 457T249 475Q318 475 366 439L408 496L455 460L409 397Q433 366 446 325T460 232Q460 177 445 133T401 56T334 7T249 -11Q216 -11 187 -2T133 25L91
-32L45 4ZM375 232Q375 260 372 285T359 330L178 85Q192 73 210 68T249 63Q307 63 341 108T375 232ZM125 232Q125 201 128 178T141 135L321 379Q307 391 289 396T249 402Q192 402 159 357T125 232Z" />
<glyph unicode="&#xf9;" glyph-name="ugrave" d="M429 13Q402 6 358 -2T253 -10Q200 -10 165 5T108 49T77 117T68 203V464H150V221Q150 135 175 99T261 63Q274 63 287 64T313 66T334 69T346 72V464H429V13ZM216 693L330 555L288 518L163 640L216 693Z" />
<glyph unicode="&#xfa;" glyph-name="uacute" d="M429 13Q402 6 358 -2T253 -10Q200 -10 165 5T108 49T77 117T68 203V464H150V221Q150 135 175 99T261 63Q274 63 287 64T313 66T334 69T346 72V464H429V13ZM347 640L221 518L179 555L293 693L347 640Z" />
<glyph unicode="&#xfb;" glyph-name="ucircumflex" d="M429 13Q402 6 358 -2T253 -10Q200 -10 165 5T108 49T77 117T68 203V464H150V221Q150 135 175 99T261 63Q274 63 287 64T313 66T334 69T346 72V464H429V13ZM250 687L371 559L337 520L250 598L163 520L129
559L250 687Z" />
<glyph unicode="&#xfc;" glyph-name="udieresis" d="M429 13Q402 6 358 -2T253 -10Q200 -10 165 5T108 49T77 117T68 203V464H150V221Q150 135 175 99T261 63Q274 63 287 64T313 66T334 69T346 72V464H429V13ZM165 550Q145 550 129 565T113 603Q113 626 129 640T165
655Q186 655 201 641T217 603Q217 580 202 565T165 550ZM335 550Q314 550 299 565T283 603Q283 626 298 640T335 655Q356 655 371 641T387 603Q387 580 372 565T335 550Z" />
<glyph unicode="&#xfd;" glyph-name="yacute" d="M51 -87Q60 -92 76 -95T107 -98Q156 -98 183 -77T234 -6Q178 100 129 221T48 464H138Q148 424 161 378T192 282T228 184T270 89Q286 135 300 180T326 270T350 363T375 464H461Q429 334 390 212T305 -14Q288 -54
269 -83T226 -131T174 -159T108 -168Q89 -168 67 -163T36 -154L51 -87ZM353 640L227 518L185 555L299 693L353 640Z" />
<glyph unicode="&#xfe;" glyph-name="thorn" d="M460 231Q460 178 447 134T408 57T345 7T261 -11Q224 -11 196 -2T154 18V-165H71V679L154 693V447Q183 462 210 468T263 475Q305 475 341 459T403 411T445 334T460 231ZM375 231Q375 274 366 306T341 360T302 391T253
402Q227 402 204 393T154 367V98Q168 85 195 74T251 63Q284 63 307 76T346 111T368 164T375 231Z" />
<glyph unicode="&#xff;" glyph-name="ydieresis" d="M51 -87Q60 -92 76 -95T107 -98Q156 -98 183 -77T234 -6Q178 100 129 221T48 464H138Q148 424 161 378T192 282T228 184T270 89Q286 135 300 180T326 270T350 363T375 464H461Q429 334 390 212T305 -14Q288
-54 269 -83T226 -131T174 -159T108 -168Q89 -168 67 -163T36 -154L51 -87ZM180 550Q160 550 144 565T128 603Q128 626 144 640T180 655Q201 655 216 641T232 603Q232 580 217 565T180 550ZM350 550Q329 550 314 565T298 603Q298 626 313 640T350 655Q371 655 386
641T402 603Q402 580 387 565T350 550Z" />
<glyph unicode="&#x2013;" glyph-name="endash" d="M69 306H431V236H69V306Z" />
<glyph unicode="&#x2014;" glyph-name="emdash" d="M0 306H500V236H0V306Z" />
<glyph unicode="&#x2018;" glyph-name="quoteleft" d="M351 645Q333 641 316 637T283 626T256 606T237 575Q269 572 283 551T298 508Q298 468 275 448T224 427Q187 427 168 453T149 518Q149 547 160 578T195 637T253 684T336 710L351 645Z" />
<glyph unicode="&#x2019;" glyph-name="quoteright" d="M148 489Q166 493 183 497T216 508T243 527T262 559Q230 562 216 583T201 626Q201 666 224 686T275 707Q312 707 331 681T350 616Q350 587 339 556T304 497T245 450T163 424L148 489Z" />
<glyph unicode="&#x201a;" glyph-name="quotesinglbase" d="M148 -79Q166 -75 183 -71T216 -60T243 -41T262 -9Q230 -6 216 15T201 58Q201 98 224 118T275 139Q312 139 331 113T350 48Q350 19 339 -12T304 -71T245 -118T163 -144L148 -79Z" />
<glyph unicode="&#x201c;" glyph-name="quotedblleft" d="M458 635Q441 631 425 628T396 617T371 600T354 571Q383 568 397 549T412 505Q412 480 396 460T346 439Q317 439 296 459T274 523Q274 551 282 579T310 630T362 670T444 693L458 635ZM227 635Q210 631
194 628T164 617T139 600T122 571Q151 568 165 549T180 505Q180 480 164 460T114 439Q85 439 64 459T43 523Q43 551 51 579T79 630T131 670T213 693L227 635Z" />
<glyph unicode="&#x201d;" glyph-name="quotedblright" d="M42 497Q59 501 75 504T104 515T129 533T146 562Q117 564 103 584T88 627Q88 652 104 672T154 693Q183 693 204 673T226 609Q226 580 218 553T190 502T138 462T56 439L42 497ZM273 497Q290 501 306 504T336
515T361 533T378 562Q349 564 335 584T320 627Q320 652 336 672T386 693Q415 693 436 673T457 609Q457 580 449 553T421 502T369 462T288 439L273 497Z" />
<glyph unicode="&#x201e;" glyph-name="quotedblbase" d="M42 -74Q76 -67 104 -56T146 -10Q117 -7 103 12T88 55Q88 80 104 100T154 121Q183 121 204 102T226 38Q226 9 218 -18T190 -69T138 -109T56 -132L42 -74ZM274 -74Q291 -70 307 -67T337 -56T362 -39T379
-10Q350 -7 336 12T321 55Q321 80 337 100T387 121Q416 121 437 102T458 38Q458 9 450 -18T422 -69T370 -109T288 -132L274 -74Z" />
<glyph unicode="&#x2022;" glyph-name="bullet" d="M386 316Q386 287 377 261T350 216T307 185T250 173Q218 173 193 184T151 215T124 261T114 316Q114 344 123 370T150 416T193 447T250 459Q281 459 306 448T349 416T376 370T386 316Z" />
<glyph unicode="&#x2039;" glyph-name="guilsinglleft" d="M144 254L283 451L341 420L242 254L341 88L283 56L144 254Z" />
<glyph unicode="&#x203a;" glyph-name="guilsinglright" d="M217 56L159 88L258 254L159 420L217 451L356 254L217 56Z" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="128px" height="128px" id="RSSicon" viewBox="0 0 256 256">
<defs>
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
<stop offset="0.0" stop-color="#E3702D"/><stop offset="0.1071" stop-color="#EA7D31"/>
<stop offset="0.3503" stop-color="#F69537"/><stop offset="0.5" stop-color="#FB9E3A"/>
<stop offset="0.7016" stop-color="#EA7C31"/><stop offset="0.8866" stop-color="#DE642B"/>
<stop offset="1.0" stop-color="#D95B29"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15"/>
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/>
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)"/>
<circle cx="68" cy="189" r="24" fill="#FFF"/>
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF"/>
<path d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg height="487.74286" viewBox="0 0 129.04864 129.04862" width="487.74286" xmlns="http://www.w3.org/2000/svg"><g fill="#07b"><path d="m0 0v-24.836l19.402 12.418z" transform="matrix(.93231393 0 0 -.93231393 21.406123 58.093374)"/><path d="m0 0v9.364c0 8.569 6.88 15.539 15.449 15.539 8.568 0 15.447-6.97 15.447-15.539v-9.364zm54.166 0h-9.676v10.364c0 16.064-12.978 29.132-29.042 29.132-16.063 0-29.042-13.068-29.042-29.132v-10.364h-8.926c-4.358 0-7.905-3.547-7.905-7.905v-5.472l26.736-17.11 7.333-4.694 12.179-7.795 12.18 7.795 7.333 4.694 26.736 17.111v5.471c0 4.358-3.547 7.905-7.906 7.905" transform="matrix(.93231393 0 0 -.93231393 49.771579 36.871396)"/><path d="m0 0v24.836l-19.403-12.418z" transform="matrix(.93231393 0 0 -.93231393 107.642482 81.248218)"/><path d="m0 0c-.963-.617-2.225-.925-3.486-.925-1.262 0-2.524.308-3.487.925l-16.026 10.257-26.735-17.111v-4.681c0-4.359 3.546-7.905 7.905-7.905h7.115l.004-27.491c0-1.499 1.76-2.306 2.896-1.329l33.506 28.82h33.164c4.36 0 7.907 3.546 7.907 7.905v4.681l-26.737 17.111z" transform="matrix(.93231393 0 0 -.93231393 67.774168 83.60822)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.45"
width="70"
height="92"
version="1.0"
sodipodi:docbase="D:\Desktop"
sodipodi:docname="GnuPG-Logo.png"
sodipodi:modified="true">
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5" />
<sodipodi:namedview
inkscape:window-height="975"
inkscape:window-width="1280"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
guidetolerance="10.0"
gridtolerance="10.0"
objecttolerance="10.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
width="70px"
height="92px"
inkscape:zoom="4"
inkscape:cx="37.678772"
inkscape:cy="50.578436"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:current-layer="svg2" />
<rect
style="opacity:1;fill:#0093dd;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect3132"
width="70"
height="47.099998"
x="0"
y="44.900002" />
<rect
style="opacity:1;fill:#0093dd;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4103"
width="15.009007"
height="19.485378"
x="4.7396865"
y="25.468845" />
<rect
style="opacity:1;fill:#0093dd;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4105"
width="15.18455"
height="19.239996"
x="50.117794"
y="25.694933" />
<path
style="opacity:1;fill:#0093dd;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 35.625,0 C 13.155955,0.16164779 4.75,20.998357 4.75,25.5625 C 10.896546,25.562499 17.561658,25.90625 18.96875,25.90625 C 19.298759,25.90625 19.367598,25.906322 19.6875,25.90625 C 21.365862,19.035999 27.552617,13.9375 34.9375,13.9375 C 42.289526,13.9375 48.445364,18.988532 50.15625,25.8125 C 50.543323,25.812313 50.720292,25.8125 51.125,25.8125 C 63.764162,25.8125 65.25,25.90625 65.25,25.90625 C 65.25,25.90625 60.201152,-0.17554394 35.625,0 z "
id="path6045" />
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1"
d="M 10.269321,25.20553 C 21.691029,1.9172856 37.700972,5.1573508 46.958242,6.5921309 C 46.958242,6.5921309 34.785164,0.76242296 23.084029,6.5100997 C 11.608567,12.146925 10.269321,25.20553 10.269321,25.20553 z "
id="path7026"
sodipodi:nodetypes="ccsc" />
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1"
d="M -0.25,68.25 C -0.25,68.25 7.5390098,59.548683 13.8125,57.25 C 22,54.25 32,55.1875 48.5625,51.875 C 54.425075,50.702485 56.25,49.25 65.1875,44.625 C 65.942492,44.234305 70.5625,44.3125 70.5625,44.3125 L 70.1875,50.5625 C 60,61.25 36.75,67.21875 34.8125,67.125 C 54.479984,69.763423 64.97809,59.568082 69.25,58.9375 C 59.3125,80.0625 24.0625,79 24.0625,79 C 44.5,84.5625 61.125,76.25 61.125,76.25 C 51.8125,91.65625 17.4375,89.5625 17.4375,89.5625 C 13.84375,89.875 10.9375,92.1875 10.9375,92.1875 L -0.125,92.5 L -0.25,68.25 z "
id="path7997"
sodipodi:nodetypes="cssscccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="325"
height="353"
id="svg2">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs34" />
<path
d="M 100.25,67.99952 H 52.3 c 1.047,-0.933 2.105,-1.85 3.175,-2.75 h 44.775 v 2.75 z m 0,61 v 18.75 h -80.5 v -39.45 c 1.324,-2.403 2.724,-4.778 4.2,-7.125 v 27.825 h 76.3 z"
id="path5"
style="fill:#94c061" />
<path
d="m 19.75,108.29952 v 39.45 h 80.5 v -18.75 h 3.7 v -61 h -3.7 v -2.75 H 55.475 c 1.307,-1.109 2.632,-2.192 3.975,-3.25 2.202,-1.742 4.435,-3.408 6.7,-5 4.153,-2.919 8.412,-5.585 12.775,-8 9.974,-5.517 20.499,-9.717 31.575,-12.6 12.134,-3.167 24.934,-4.75 38.4,-4.75 9.199,0 18.083,0.75 26.649,2.25 3,0.5 6,1.117 9,1.85 24.967,6 47.217,18.583 66.75,37.75 0.334,0.333 0.667,0.667 1,1 21.334,21.367 34.717,45.983 40.15,73.85 0.2,1.167 0.416,2.351 0.649,3.551 1.4,8.3 2.101,16.85 2.101,25.649 0,0.134 0,0.283 0,0.45 0,22.934 -4.601,43.967 -13.8,63.1 -4.601,9.434 -10.284,18.417 -17.051,26.95 -2,2.4 -4.033,4.783 -6.1,7.15 -1.934,2.133 -3.917,4.217 -5.95,6.25 -3.5,3.5 -7.066,6.8 -10.7,9.899 -4.833,4.034 -9.833,7.717 -15,11.051 -5.333,3.399 -10.833,6.416 -16.5,9.05 -0.533,0.2 -1.033,0.416 -1.5,0.649 -17.466,7.867 -36.483,11.967 -57.05,12.301 -0.533,0 -1.05,0 -1.55,0 -0.366,0 -0.733,0 -1.1,0 -40.434,0 -74.934,-14.317 -103.5,-42.95 -0.467,-0.467 -0.917,-0.917 -1.35,-1.351 -27.7,-28.3 -41.55,-62.333 -41.55,-102.1 0,-0.167 0,-0.316 0,-0.45 0.042,-14.928 2.042,-29.053 6,-42.375 0.517,-1.738 1.067,-3.464 1.65,-5.175 2.562,-7.495 5.762,-14.728 9.602,-21.699 z"
id="path7"
style="fill:#658d38" />
<path
d="m 91.3,117.19952 c -0.614,-0.905 -1.206,-1.821 -1.775,-2.75 0.907,-1.061 1.598,-2.144 2.075,-3.25 1.167,-2.333 1.75,-4.9 1.75,-7.7 0,-1.033 -0.083,-2.033 -0.25,-3 -0.368,-2.332 -1.21,-4.482 -2.525,-6.45 0.112,0.118 0.204,0.209 0.275,0.275 l 0.15,0.175 c 2.9,3.256 4.35,7.09 4.35,11.5 0,1.759 -0.233,3.425 -0.7,5 -0.266,0.932 -0.616,1.833 -1.05,2.7 -0.515,1.195 -1.282,2.362 -2.3,3.5 z m -7.475,5.225 c -1.841,0.65 -3.816,0.975 -5.925,0.975 -2.68,0 -5.146,-0.533 -7.4,-1.6 -1.783,-0.85 -3.433,-2.033 -4.95,-3.55 -0.894,-0.895 -1.669,-1.836 -2.325,-2.825 0.106,0.106 0.214,0.214 0.325,0.325 3.434,3.433 7.55,5.15 12.35,5.15 2.171,0 4.205,-0.35 6.1,-1.05 0.588,0.867 1.197,1.725 1.825,2.575 z"
id="path10"
style="fill:#bbd89c" />
<path
d="m 120.575,140.84952 c 0.758,-1.002 1.6,-1.968 2.524,-2.899 4.9,-4.867 10.801,-7.3 17.7,-7.3 3.473,0 6.689,0.616 9.65,1.85 -0.393,-0.553 -0.81,-1.095 -1.25,-1.625 0.193,0.112 0.385,0.229 0.575,0.35 0.349,0.262 0.69,0.537 1.024,0.825 1.32,1.517 2.354,3.133 3.101,4.851 0.1,0.199 0.199,0.383 0.3,0.55 1.1,2.033 1.866,4.184 2.3,6.45 0.063,0.356 0.121,0.715 0.175,1.074 -3.332,0.468 -6.757,0.7 -10.274,0.7 -9.27,0.001 -17.879,-1.608 -25.825,-4.826 z m 102.55,-21.8 c -0.209,-0.221 -0.417,-0.438 -0.625,-0.65 -1.628,-1.637 -3.294,-3.195 -5,-4.675 2.639,-5.218 4.613,-10.71 5.925,-16.475 l 25.851,-25.775 c 0.342,0.343 0.684,0.685 1.024,1.025 21.334,21.367 34.717,45.983 40.15,73.85 0.2,1.167 0.416,2.351 0.649,3.551 1.4,8.3 2.101,16.85 2.101,25.649 0,0.134 0,0.283 0,0.45 0,22.934 -4.601,43.967 -13.8,63.1 -4.601,9.434 -10.284,18.417 -17.051,26.95 -0.075,0.091 -0.15,0.183 -0.225,0.275 l 5.975,-21.976 c 0.101,-0.399 0.051,-0.816 -0.149,-1.25 -0.167,-0.366 -0.417,-0.683 -0.75,-0.949 -0.134,-0.034 -8.167,-8.784 -24.101,-26.25 3.448,-7.858 5.157,-14.851 5.125,-20.976 -0.005,-1.372 -0.005,-2.605 0,-3.7 0.547,-2.661 0.964,-5.245 1.25,-7.75 0.469,-4.224 0.561,-8.232 0.275,-12.024 0.033,-0.367 0.033,-0.783 0,-1.25 -0.4,-2.634 -0.884,-5.25 -1.45,-7.851 -2.666,-11.433 -7.75,-22.116 -15.25,-32.05 -1.333,-1.7 -2.7,-3.383 -4.1,-5.05 -1.467,-1.7 -2.95,-3.317 -4.45,-4.85 -0.455,-0.458 -0.913,-0.908 -1.374,-1.349 z m -181.075,159.05 46.725,-46.574 c 1.695,2.331 3.52,4.623 5.475,6.875 0.874,0.999 1.757,1.975 2.65,2.925 l 51.075,81.075 c -0.362,0 -0.721,0 -1.074,0 -40.434,0 -74.934,-14.317 -103.5,-42.95 -0.454,-0.453 -0.903,-0.903 -1.351,-1.351 z m 113.55,-122.05 c -1.316,3.908 -3.649,7.324 -7,10.25 -0.066,0.033 -0.149,0.117 -0.25,0.25 -0.1,0.033 -0.149,0.084 -0.149,0.15 -0.101,0.033 -0.167,0.083 -0.2,0.149 -0.434,0.367 -0.866,0.733 -1.3,1.101 -0.101,0.033 -0.2,0.1 -0.3,0.2 -2.434,1.8 -4.9,3.017 -7.4,3.649 l -2.85,0.601 c -0.267,0 -0.517,0.033 -0.75,0.1 -1,0.1 -2.034,0.15 -3.101,0.15 -0.1,0 -0.2,0 -0.3,0 -0.1,0 -0.2,0 -0.3,0 -0.101,0 -0.2,0 -0.3,0 -0.134,-0.067 -0.284,-0.101 -0.45,-0.101 h -0.25 c -4.182,-0.275 -7.89,-1.476 -11.125,-3.6 -2.517,-3.904 -3.775,-8.338 -3.775,-13.3 0,-2.092 0.226,-4.092 0.675,-6 9.717,4.448 20.358,6.682 31.926,6.699 2.441,-0.001 4.841,-0.102 7.199,-0.298 z"
id="path12"
style="fill:#7bad45" />
<path
d="m 88.775,231.52452 -46.725,46.575 c -27.7,-28.304 -41.55,-62.337 -41.55,-102.1 0,-0.167 0,-0.316 0,-0.45 0.089,-36.006 11.589,-67.339 34.5,-94 1.905,-2.214 3.888,-4.397 5.95,-6.55 0.805,-0.837 1.622,-1.671 2.45,-2.5 4.5,-4.5 9.184,-8.667 14.05,-12.5 0.165,-0.13 0.332,-0.264 0.5,-0.4 0.336,-0.264 0.669,-0.522 1,-0.775 0.154,-0.119 0.304,-0.235 0.45,-0.35 l 13.15,-8.95 c 0.752,-0.458 1.511,-0.908 2.275,-1.35 v 0.025 c 0.41,-0.238 0.818,-0.479 1.225,-0.725 l 1.225,-0.675 c 0.469,-0.25 0.935,-0.5 1.4,-0.75 4.524,-2.416 9.158,-4.565 13.9,-6.45 4.045,-1.601 8.171,-3.009 12.375,-4.225 1.177,-0.34 2.36,-0.665 3.55,-0.975 12.141,-3.167 24.94,-4.75 38.4,-4.75 9.199,0 18.083,0.75 26.649,2.25 3,0.5 6,1.117 9,1.85 7.557,1.815 14.865,4.231 21.925,7.25 6.836,10.689 10.252,22.79 10.25,36.3 0.002,10.708 -2.14,20.524 -6.425,29.45 -8.328,-5.511 -17.444,-9.411 -27.35,-11.7 -0.233,-0.066 -0.45,-0.117 -0.65,-0.15 -1.434,-0.333 -2.916,-0.633 -4.45,-0.9 -0.199,-0.033 -0.35,-0.05 -0.449,-0.05 -5.301,-0.7 -8.25,-1.067 -8.851,-1.1 h -0.05 c -18.967,-1.267 -36.134,2.783 -51.5,12.15 -4.233,2.567 -8.316,5.55 -12.25,8.95 -0.1,0.066 -0.167,0.15 -0.2,0.25 -0.133,0.1 -0.283,0.233 -0.45,0.4 -0.53,0.462 -1.056,0.929 -1.574,1.4 -2.051,1.869 -3.984,3.803 -5.8,5.8 -1.214,-1.499 -2.355,-3.032 -3.425,-4.6 1.019,-1.139 1.785,-2.305 2.3,-3.5 0.434,-0.868 0.784,-1.768 1.05,-2.7 0.467,-1.575 0.7,-3.241 0.7,-5 0,-4.41 -1.45,-8.244 -4.35,-11.5 l -0.15,-0.175 c -0.071,-0.066 -0.163,-0.158 -0.275,-0.275 -0.069,-0.076 -0.144,-0.159 -0.225,-0.25 -0.051,-0.051 -0.102,-0.101 -0.15,-0.15 -0.633,-0.633 -1.317,-1.217 -2.05,-1.75 -2.26,-1.695 -4.768,-2.728 -7.525,-3.1 H 80.6 c -0.432,-0.1 -0.898,-0.149 -1.4,-0.15 -0.434,-0.066 -0.867,-0.1 -1.3,-0.1 -0.333,0 -0.65,0.034 -0.95,0.1 -0.667,0 -1.317,0.05 -1.95,0.15 h -0.05 c -0.256,0.041 -0.506,0.091 -0.75,0.15 l -2.2,0.6 c -0.44,0.165 -0.874,0.348 -1.3,0.55 -0.117,0.05 -0.233,0.1 -0.35,0.15 -1.731,0.798 -3.332,1.931 -4.8,3.4 -0.2,0.2 -0.383,0.417 -0.55,0.65 -0.062,0.066 -0.12,0.133 -0.175,0.2 -1.904,2.117 -3.179,4.5 -3.825,7.15 -0.333,1.4 -0.5,2.85 -0.5,4.35 0,3.525 0.908,6.667 2.725,9.425 0.656,0.989 1.431,1.931 2.325,2.825 1.517,1.517 3.167,2.7 4.95,3.55 2.253,1.067 4.72,1.6 7.4,1.6 2.109,0 4.084,-0.325 5.925,-0.975 1.599,2.163 3.333,4.271 5.2,6.325 0.039,-0.053 0.081,-0.103 0.125,-0.15 -0.357,0.478 -0.708,0.961 -1.05,1.45 -0.7,1 -1.35,2 -1.95,3 -1.6,2.434 -3.083,4.967 -4.45,7.601 -0.016,0.033 -0.033,0.066 -0.05,0.1 -1.014,2.101 -1.964,4.217 -2.85,6.35 -0.633,1.601 -1.216,3.233 -1.75,4.9 -2.267,6.967 -3.683,14.384 -4.25,22.25 -1.522,21.336 3.803,40.428 15.975,57.274 z m 160.5,-160.05 -25.851,25.775 c 1.307,-5.716 1.966,-11.699 1.976,-17.95 -0.018,-11.181 -2.109,-21.498 -6.275,-30.95 10.682,6.175 20.731,13.884 30.15,23.125 z m -100.075,59.4 c -0.057,-0.069 -0.115,-0.136 -0.175,-0.2 0.254,0.176 0.504,0.359 0.75,0.55 -0.19,-0.121 -0.381,-0.238 -0.575,-0.35 z"
id="path14"
style="fill:#94c061" />
<path
d="m 220,303.19952 c -3.879,2.285 -7.846,4.368 -11.9,6.25 -0.533,0.2 -1.033,0.416 -1.5,0.649 -17.453,7.864 -36.47,11.956 -57.05,12.275 -0.523,0.01 -1.049,0.019 -1.575,0.025 L 96.9,241.32452 c 0.931,0.984 1.873,1.943 2.825,2.875 2.176,2.226 4.417,4.309 6.725,6.25 2.767,2.366 5.684,4.533 8.75,6.5 10.033,6.566 21.25,10.816 33.649,12.75 0.101,0 0.233,0 0.4,0 5.441,-0.049 10.25,-0.207 14.425,-0.476 1.525,-0.091 2.968,-0.199 4.325,-0.324 5.1,-0.467 10.3,-1.184 15.6,-2.15 l 1.4,1.4 v 0.699 c 0.2,-0.03 0.399,-0.063 0.6,-0.1 l 34.401,34.451 z"
id="path16"
style="fill:#6c983d" />
<path
d="m 201.8,23.87452 c -14.804,-14.35 -32.604,-21.525 -53.399,-21.525 -21.301,0 -39.467,7.5 -54.5,22.5 -3.536,3.543 -6.652,7.26 -9.35,11.15 -2.247,3.233 -4.206,6.583 -5.875,10.05 -0.465,0.25 -0.931,0.5 -1.4,0.75 l -1.225,0.675 c -0.406,0.246 -0.815,0.487 -1.225,0.725 v -0.025 c 3.759,-9.244 9.451,-17.686 17.075,-25.325 15.042,-15.012 33.208,-22.521 54.5,-22.525 21.259,0.003 39.392,7.512 54.399,22.525 0.337,0.337 0.67,0.678 1,1.025 z m -6.1,6.075 c 0.331,0.331 0.664,0.665 1,1 3.316,3.316 6.225,6.799 8.725,10.45 7.55,11.081 11.325,23.714 11.325,37.9 0,10.513 -2.066,20.171 -6.2,28.975 -0.072,0.155 -0.147,0.314 -0.225,0.475 -0.525,1.09 -1.084,2.165 -1.675,3.225 -3.098,5.593 -7.081,10.818 -11.95,15.675 -4.908,4.92 -10.191,8.937 -15.851,12.05 -5.304,2.91 -10.937,5.027 -16.899,6.35 -1.729,0.384 -3.486,0.7 -5.275,0.95 -0.607,0.085 -1.216,0.16 -1.825,0.225 -2.757,0.317 -5.573,0.476 -8.449,0.476 -10.305,0 -19.788,-1.983 -28.45,-5.95 -2.097,-0.97 -4.146,-2.053 -6.15,-3.25 l -0.325,0.55 c -0.111,0.182 -0.22,0.365 -0.324,0.55 0.322,-0.596 0.673,-1.18 1.05,-1.75 2.078,1.124 4.203,2.124 6.375,3 7.946,3.219 16.555,4.827 25.825,4.825 3.518,0 6.942,-0.232 10.274,-0.7 0.526,-0.073 1.052,-0.156 1.575,-0.25 1.565,-0.254 3.106,-0.562 4.625,-0.925 11.948,-2.813 22.557,-8.863 31.825,-18.15 5.637,-5.626 10.078,-11.743 13.325,-18.35 0.096,-0.186 0.188,-0.369 0.274,-0.55 4.285,-8.926 6.427,-18.742 6.425,-29.45 0.002,-13.51 -3.414,-25.61 -10.25,-36.3 -2.478,-3.868 -5.403,-7.551 -8.775,-11.051 z m -113.7,89.9 c -6.065,-8.954 -9.948,-18.82 -11.65,-29.6 0.117,-0.05 0.233,-0.1 0.35,-0.15 0.426,-0.203 0.859,-0.386 1.3,-0.55 1.337,10.945 4.862,20.97 10.575,30.075 0.549,0.882 1.115,1.757 1.7,2.625 0.852,1.233 1.744,2.45 2.675,3.65 0.707,0.909 1.44,1.809 2.2,2.7 -0.044,0.047 -0.086,0.097 -0.125,0.15 -1.867,-2.054 -3.601,-4.163 -5.2,-6.325 -0.628,-0.85 -1.237,-1.708 -1.825,-2.575 z"
id="path18"
style="fill:#e2e2e2" />
<path
d="m 92.575,39.59952 c -4.743,1.884 -9.376,4.034 -13.9,6.45 1.669,-3.466 3.628,-6.816 5.875,-10.05 2.698,-3.89 5.814,-7.606 9.35,-11.15 15.034,-15 33.2,-22.5 54.5,-22.5 20.796,0 38.596,7.175 53.399,21.525 0.335,0.318 0.668,0.643 1,0.975 7.129,7.129 12.571,14.962 16.325,23.5 4.166,9.452 6.258,19.769 6.275,30.95 -0.01,6.251 -0.669,12.234 -1.976,17.95 -1.312,5.765 -3.286,11.257 -5.925,16.475 -0.037,0.074 -0.07,0.148 -0.1,0.225 -3.637,7.117 -8.504,13.716 -14.601,19.8 -10.662,10.686 -22.903,17.577 -36.725,20.675 -0.104,0.029 -0.203,0.054 -0.3,0.075 -2.47,0.547 -4.986,0.972 -7.551,1.275 -0.869,0.103 -1.744,0.194 -2.625,0.274 -2.357,0.196 -4.758,0.297 -7.199,0.3 -11.567,-0.018 -22.209,-2.251 -31.926,-6.699 -1.929,-0.879 -3.821,-1.846 -5.675,-2.9 0.047,-0.277 0.098,-0.552 0.15,-0.825 0.015,-0.092 0.031,-0.184 0.05,-0.274 0.173,-0.857 0.39,-1.69 0.65,-2.5 0.388,-1.194 0.871,-2.336 1.449,-3.426 l 0.051,-0.125 c 0.104,-0.185 0.213,-0.368 0.324,-0.55 l 0.325,-0.55 c 2.004,1.197 4.054,2.28 6.15,3.25 8.662,3.967 18.146,5.95 28.45,5.95 2.876,0 5.692,-0.158 8.449,-0.476 0.609,-0.064 1.218,-0.14 1.825,-0.225 1.789,-0.25 3.547,-0.566 5.275,-0.95 5.963,-1.322 11.596,-3.439 16.899,-6.35 5.659,-3.113 10.942,-7.13 15.851,-12.05 4.869,-4.857 8.853,-10.082 11.95,-15.675 0.591,-1.061 1.149,-2.135 1.675,-3.225 0.077,-0.161 0.152,-0.32 0.225,-0.475 4.134,-8.804 6.2,-18.462 6.2,-28.975 0,-14.186 -3.775,-26.819 -11.325,-37.9 -2.5,-3.65 -5.408,-7.134 -8.725,-10.45 -0.336,-0.335 -0.669,-0.669 -1,-1 -13.142,-12.666 -28.908,-18.983 -47.3,-18.95 -18.9,-0.033 -35.034,6.617 -48.4,19.95 -2.754,2.768 -5.228,5.651 -7.419,8.651 z m 115.775,211.8 -3.1,2.449 c -0.2,0.134 -0.366,0.25 -0.5,0.351 -0.267,0.2 -0.517,0.366 -0.75,0.5 -1.134,0.733 -2.267,1.467 -3.4,2.2 -0.1,0.033 -0.183,0.083 -0.25,0.149 l 65.601,65.75 c 0.133,0.134 0.316,0.316 0.55,0.55 l 1.1,1.101 c 1.4,1.366 3.067,2.05 5,2.05 1.634,-0.066 3.051,-0.55 4.25,-1.45 0.301,-0.166 0.551,-0.383 0.75,-0.649 0.233,-0.167 1.25,0.399 3.051,1.699 1.767,1.334 5.116,4.817 10.05,10.45 4.866,5.533 11.833,9.533 20.899,12 -5.933,1 -14.783,1.7 -26.55,2.101 -11.121,0.349 -19.721,-2.201 -25.8,-7.65 -1.462,-1.316 -2.778,-2.8 -3.95,-4.45 l -20.1,-20.1 h 0.05 l -15.251,-15.251 -34.4,-34.45 -0.6,-0.6 -1.4,-1.4 c -5.3,0.967 -10.5,1.684 -15.6,2.15 -1.357,0.125 -2.8,0.233 -4.325,0.324 l 33.95,-33.925 c 0.542,0.528 1.075,1.045 1.6,1.55 4.902,4.798 8.96,8.848 12.176,12.15 -0.167,0.066 -0.284,0.167 -0.351,0.3 -0.899,0.733 -1.767,1.45 -2.6,2.15 l -0.1,-0.049 z m -119.2,-122.8 c -0.759,-0.891 -1.493,-1.791 -2.2,-2.7 -0.932,-1.2 -1.823,-2.417 -2.675,-3.65 -0.584,-0.868 -1.151,-1.743 -1.7,-2.625 -5.713,-9.105 -9.238,-19.13 -10.575,-30.075 l 2.2,-0.6 c 0.244,-0.06 0.494,-0.109 0.75,-0.15 H 75 c 0.633,-0.1 1.283,-0.15 1.95,-0.15 0.3,-0.066 0.617,-0.1 0.95,-0.1 0.434,0 0.867,0.034 1.3,0.1 0.501,0 0.968,0.05 1.4,0.15 h 0.025 c 0.946,7.487 3.054,14.487 6.325,21 0.793,1.581 1.651,3.131 2.575,4.65 0.569,0.929 1.161,1.845 1.775,2.75 1.07,1.567 2.211,3.101 3.425,4.6 -0.236,0.257 -0.469,0.515 -0.7,0.775 l -4.875,6.025 z"
id="path20"
style="fill:#d3d3d3" />
<path
d="m 96.9,241.32452 c -0.893,-0.95 -1.776,-1.926 -2.65,-2.925 -1.955,-2.252 -3.78,-4.544 -5.475,-6.875 -12.172,-16.847 -17.497,-35.938 -15.975,-57.275 0.567,-7.866 1.983,-15.283 4.25,-22.25 0.534,-1.667 1.117,-3.3 1.75,-4.9 0.886,-2.133 1.836,-4.249 2.85,-6.35 0.017,-0.033 0.034,-0.066 0.05,-0.1 1.367,-2.634 2.85,-5.167 4.45,-7.601 0.6,-1 1.25,-2 1.95,-3 0.342,-0.488 0.692,-0.972 1.05,-1.45 l 4.875,-6.025 c 0.231,-0.26 0.464,-0.519 0.7,-0.775 1.816,-1.997 3.75,-3.931 5.8,-5.8 0.519,-0.471 1.044,-0.938 1.574,-1.4 0.167,-0.167 0.317,-0.3 0.45,-0.4 0.033,-0.1 0.101,-0.184 0.2,-0.25 3.934,-3.4 8.017,-6.383 12.25,-8.95 15.366,-9.367 32.533,-13.417 51.5,-12.15 h 0.05 c 0.601,0.033 3.55,0.4 8.851,1.1 0.1,0 0.25,0.017 0.449,0.05 1.534,0.267 3.017,0.567 4.45,0.9 0.2,0.033 0.417,0.083 0.65,0.15 9.905,2.289 19.021,6.189 27.35,11.7 -0.087,0.181 -0.179,0.364 -0.274,0.55 -7.707,-4.751 -16.065,-8.167 -25.075,-10.25 -0.233,-0.066 -0.45,-0.117 -0.65,-0.15 -1.434,-0.333 -2.916,-0.633 -4.45,-0.9 -0.199,-0.033 -0.35,-0.05 -0.449,-0.05 -5.301,-0.7 -8.25,-1.067 -8.851,-1.1 h -0.05 c -18.98,-1.281 -36.146,2.769 -51.5,12.15 -4.22,2.58 -8.303,5.563 -12.25,8.95 -0.1,0.066 -0.167,0.15 -0.2,0.25 -0.133,0.1 -0.283,0.233 -0.45,0.4 -5.433,4.733 -10.1,9.883 -14,15.45 -0.7,1 -1.35,2 -1.95,3 -1.6,2.434 -3.083,4.967 -4.45,7.601 -1.034,2.133 -2,4.283 -2.9,6.449 -0.634,1.608 -1.217,3.242 -1.75,4.9 -2.268,6.971 -3.685,14.388 -4.25,22.25 -1.451,20.351 3.324,38.659 14.325,54.925 2.144,3.148 4.519,6.224 7.125,9.226 0.08,0.091 0.163,0.183 0.25,0.274 1.057,1.209 2.132,2.384 3.225,3.525 -0.952,-0.93 -1.894,-1.889 -2.825,-2.874 z m 53.9,-109.275 c 0.185,0.165 0.368,0.332 0.55,0.5 2.034,1.934 3.551,4.05 4.551,6.351 0.1,0.199 0.199,0.383 0.3,0.55 0.91,1.683 1.594,3.44 2.05,5.274 -0.523,0.094 -1.049,0.177 -1.575,0.25 -0.054,-0.359 -0.112,-0.718 -0.175,-1.074 -0.434,-2.267 -1.2,-4.417 -2.3,-6.45 -0.101,-0.167 -0.2,-0.351 -0.3,-0.55 -0.748,-1.718 -1.781,-3.335 -3.101,-4.851 z m -33.9,34.85 c 0.853,0.762 1.744,1.445 2.675,2.05 3.235,2.124 6.943,3.324 11.125,3.6 h 0.25 c 0.166,0 0.316,0.033 0.45,0.101 0.1,0 0.199,0 0.3,0 0.1,0 0.2,0 0.3,0 0.1,0 0.2,0 0.3,0 1.066,0 2.101,-0.051 3.101,-0.15 0.233,-0.066 0.483,-0.1 0.75,-0.1 l 2.849,-0.601 c 2.5,-0.633 4.967,-1.85 7.4,-3.649 0.1,-0.101 0.199,-0.167 0.3,-0.2 0.434,-0.367 0.866,-0.733 1.3,-1.101 0.033,-0.066 0.1,-0.116 0.2,-0.149 0,-0.066 0.05,-0.117 0.149,-0.15 0.101,-0.133 0.184,-0.217 0.25,-0.25 3.351,-2.926 5.684,-6.342 7,-10.25 0.881,-0.08 1.756,-0.172 2.625,-0.274 -1.101,4.872 -3.643,9.047 -7.625,12.524 -0.066,0.033 -0.149,0.117 -0.25,0.25 -0.1,0.033 -0.149,0.084 -0.149,0.15 -0.101,0.033 -0.167,0.083 -0.2,0.149 -0.434,0.367 -0.866,0.733 -1.3,1.101 -0.101,0.033 -0.2,0.1 -0.3,0.2 -2.434,1.8 -4.9,3.017 -7.4,3.649 l -2.85,0.601 c -0.267,0 -0.517,0.033 -0.75,0.1 -1,0.1 -2.034,0.15 -3.101,0.15 -0.1,0 -0.2,0 -0.3,0 -0.1,0 -0.2,0 -0.3,0 -0.101,0 -0.2,0 -0.3,0 -0.134,-0.067 -0.284,-0.101 -0.45,-0.101 h -0.25 c -5.009,-0.33 -9.343,-1.98 -13,-4.95 -0.979,-0.805 -1.912,-1.705 -2.799,-2.7 z m 100.6,-53.175 c 1.706,1.48 3.372,3.038 5,4.675 0.208,0.212 0.416,0.429 0.625,0.65 -1.864,-1.801 -3.772,-3.501 -5.725,-5.1 0.03,-0.076 0.063,-0.151 0.1,-0.225 z"
id="path22"
style="fill:#b6b6b6" />
<path
d="m 249.475,183.47452 -51.85,51.825 -33.95,33.925 c -4.175,0.269 -8.983,0.427 -14.425,0.476 -0.167,0 -0.3,0 -0.4,0 -12.399,-1.934 -23.616,-6.184 -33.649,-12.75 -3.066,-1.967 -5.983,-4.134 -8.75,-6.5 -2.308,-1.941 -4.549,-4.024 -6.725,-6.25 -1.093,-1.142 -2.168,-2.316 -3.225,-3.525 -0.087,-0.092 -0.17,-0.184 -0.25,-0.274 -2.606,-3.002 -4.981,-6.077 -7.125,-9.226 -11.001,-16.266 -15.776,-34.574 -14.325,-54.925 0.565,-7.862 1.982,-15.279 4.25,-22.25 0.533,-1.658 1.116,-3.292 1.75,-4.9 0.9,-2.166 1.867,-4.316 2.9,-6.449 1.367,-2.634 2.85,-5.167 4.45,-7.601 0.6,-1 1.25,-2 1.95,-3 3.9,-5.566 8.566,-10.716 14,-15.45 0.167,-0.167 0.317,-0.3 0.45,-0.4 0.033,-0.1 0.101,-0.184 0.2,-0.25 3.947,-3.387 8.03,-6.37 12.25,-8.95 15.354,-9.381 32.52,-13.431 51.5,-12.15 h 0.05 c 0.601,0.033 3.55,0.4 8.851,1.1 0.1,0 0.25,0.017 0.449,0.05 1.534,0.267 3.017,0.567 4.45,0.9 0.2,0.033 0.417,0.083 0.65,0.15 9.01,2.083 17.368,5.499 25.075,10.25 -3.247,6.607 -7.688,12.724 -13.325,18.35 -9.269,9.287 -19.877,15.336 -31.825,18.15 -1.519,0.363 -3.06,0.671 -4.625,0.925 -0.456,-1.834 -1.14,-3.592 -2.05,-5.274 -0.101,-0.167 -0.2,-0.351 -0.3,-0.55 -1,-2.301 -2.517,-4.417 -4.551,-6.351 -0.182,-0.168 -0.365,-0.335 -0.55,-0.5 -0.334,-0.288 -0.676,-0.563 -1.024,-0.825 -0.246,-0.19 -0.496,-0.374 -0.75,-0.55 -3.474,-2.444 -7.615,-3.87 -12.426,-4.275 -2.301,-0.139 -4.501,0.011 -6.6,0.45 -3.92,0.822 -7.487,2.656 -10.7,5.5 -0.233,0.167 -0.45,0.351 -0.649,0.551 -1.766,1.505 -3.249,3.154 -4.45,4.949 -0.377,0.57 -0.728,1.154 -1.05,1.75 l -0.051,0.125 c -0.578,1.09 -1.062,2.231 -1.449,3.426 -0.261,0.81 -0.478,1.643 -0.65,2.5 -0.019,0.091 -0.035,0.183 -0.05,0.274 -0.053,0.273 -0.104,0.548 -0.15,0.825 -0.114,0.75 -0.198,1.517 -0.25,2.3 0,0.033 0,0.084 0,0.15 -0.09,1.646 -0.04,3.246 0.15,4.8 0.082,0.744 0.199,1.478 0.35,2.2 0.834,3.666 2.601,7.066 5.3,10.2 0.018,0.016 0.034,0.032 0.051,0.05 h 0.1 c 0.133,0.155 0.266,0.306 0.4,0.45 0.887,0.994 1.819,1.895 2.8,2.699 3.657,2.97 7.991,4.62 13,4.95 h 0.25 c 0.166,0 0.316,0.033 0.45,0.101 0.1,0 0.199,0 0.3,0 0.1,0 0.2,0 0.3,0 0.1,0 0.2,0 0.3,0 1.066,0 2.101,-0.051 3.101,-0.15 0.233,-0.066 0.483,-0.1 0.75,-0.1 l 2.847,-0.601 c 2.5,-0.633 4.967,-1.85 7.4,-3.649 0.1,-0.101 0.199,-0.167 0.3,-0.2 0.434,-0.367 0.866,-0.733 1.3,-1.101 0.033,-0.066 0.1,-0.116 0.2,-0.149 0,-0.066 0.05,-0.117 0.149,-0.15 0.101,-0.133 0.184,-0.217 0.25,-0.25 3.982,-3.478 6.524,-7.652 7.625,-12.524 2.564,-0.304 5.081,-0.729 7.551,-1.275 0.097,-0.021 0.196,-0.046 0.3,-0.075 13.821,-3.098 26.063,-9.989 36.725,-20.675 6.097,-6.083 10.964,-12.683 14.601,-19.8 1.952,1.598 3.86,3.298 5.725,5.1 0.461,0.441 0.919,0.892 1.375,1.35 1.5,1.533 2.983,3.15 4.45,4.85 1.399,1.667 2.767,3.35 4.1,5.05 7.5,9.934 12.584,20.617 15.25,32.05 0.566,2.601 1.05,5.217 1.45,7.851 0.033,0.467 0.033,0.883 0,1.25 0.284,3.789 0.192,7.798 -0.276,12.022 z"
id="path24"
style="fill:#a3a3a3" />
<path
d="m 261.175,269.89952 h -0.024 v 0.05 c -0.367,0.066 -0.75,0.033 -1.15,-0.101 -0.192,-0.096 -0.359,-0.229 -0.5,-0.399 -0.115,-0.131 -0.216,-0.281 -0.3,-0.45 v -0.15 c -0.134,-0.333 -0.134,-0.683 0,-1.05 l -0.05,0.101 6.949,-25.551 c 0.101,-0.399 0.051,-0.816 -0.149,-1.25 -0.167,-0.366 -0.417,-0.683 -0.75,-0.949 -0.134,-0.034 -8.167,-8.784 -24.101,-26.25 1.967,-3.834 4.051,-10.117 6.25,-18.851 0.32,-1.295 0.612,-2.57 0.875,-3.825 -0.005,1.095 -0.005,2.328 0,3.7 0.032,6.125 -1.677,13.117 -5.125,20.976 15.934,17.466 23.967,26.216 24.101,26.25 0.333,0.267 0.583,0.583 0.75,0.949 0.2,0.434 0.25,0.851 0.149,1.25 l -5.975,21.976 -0.975,3.575 0.05,-0.101 c -0.012,0.032 -0.02,0.065 -0.025,0.1 z m 53.775,19.45 c 6.564,5.904 9.681,14.588 9.35,26.051 -0.467,13.733 -1.383,23.517 -2.75,29.35 -0.233,1.167 -0.767,2.25 -1.6,3.25 l -0.15,0.3 c -0.333,0.233 -0.649,0.45 -0.95,0.65 -0.8,0.533 -1.666,0.866 -2.6,1 -0.8,0.2 -1.684,0.399 -2.65,0.6 -5.933,1 -14.783,1.7 -26.55,2.101 -12.402,0.389 -21.669,-2.827 -27.8,-9.65 6.079,5.449 14.679,7.999 25.8,7.65 11.767,-0.4 20.617,-1.101 26.55,-2.101 0.967,-0.2 1.851,-0.399 2.65,-0.6 0.934,-0.134 1.8,-0.467 2.6,-1 0.301,-0.2 0.617,-0.417 0.95,-0.65 l 0.15,-0.3 c 0.833,-1 1.366,-2.083 1.6,-3.25 1.367,-5.833 2.283,-15.616 2.75,-29.35 0.296,-10.23 -2.154,-18.247 -7.35,-24.051 z m -31.125,3.225 c -0.357,0.039 -0.698,0.014 -1.025,-0.075 -0.333,-0.2 -0.6,-0.467 -0.8,-0.8 -0.066,-0.066 -0.1,-0.15 -0.1,-0.25 -0.134,-0.334 -0.15,-0.667 -0.051,-1 l 0.051,-0.05 6.75,-25.051 c 0.1,-0.433 0.083,-0.85 -0.051,-1.25 1.423,1.927 2.105,3.01 2.051,3.25 l -6.75,25.051 -0.051,0.05 c -0.011,0.039 -0.019,0.081 -0.024,0.125 z m -98.225,-23.825 c -0.2,0.036 -0.399,0.069 -0.6,0.1 v -0.699 l 0.6,0.599 z"
id="path26"
style="fill:#8f8f8f" />
<path
d="m 197.625,235.29952 51.85,-51.825 c -0.286,2.505 -0.703,5.089 -1.25,7.75 -0.263,1.255 -0.555,2.53 -0.875,3.825 -2.199,8.733 -4.283,15.017 -6.25,18.851 15.934,17.466 23.967,26.216 24.101,26.25 0.333,0.267 0.583,0.583 0.75,0.949 0.2,0.434 0.25,0.851 0.149,1.25 l -6.95,25.55 0.05,-0.101 c -0.134,0.367 -0.134,0.717 0,1.05 v 0.151 c 0.084,0.169 0.185,0.319 0.3,0.45 0.141,0.17 0.308,0.304 0.5,0.399 0.4,0.134 0.783,0.167 1.15,0.101 v -0.05 h 0.024 l 25.325,-6.65 c 0.434,-0.233 0.833,-0.217 1.2,0.05 0.333,0.167 0.633,0.434 0.899,0.8 0.134,0.4 0.15,0.817 0.051,1.25 l -6.749,25.05 -0.051,0.05 c -0.1,0.333 -0.083,0.666 0.051,1 0,0.1 0.033,0.184 0.1,0.25 0.2,0.333 0.467,0.6 0.8,0.8 0.327,0.089 0.668,0.114 1.025,0.075 0.039,-0.01 0.081,-0.018 0.125,-0.025 v 0.101 l 0.1,-0.101 26.65,-7 c 1.576,1.142 2.992,2.408 4.25,3.8 5.195,5.805 7.646,13.821 7.35,24.051 -0.467,13.733 -1.383,23.517 -2.75,29.35 -0.233,1.167 -0.767,2.25 -1.6,3.25 l -0.15,0.3 c -0.333,0.233 -0.649,0.45 -0.95,0.65 -0.8,0.533 -1.666,0.866 -2.6,1 -0.8,0.2 -1.684,0.399 -2.65,0.6 -21.5,-21.6 -32.816,-32.934 -33.949,-34 l -60.101,-60.351 c -0.2,-0.101 -0.366,-0.233 -0.5,-0.4 l -5.2,-5.25 -0.149,0.15 c -0.101,0.066 -0.2,0.166 -0.3,0.3 -3.216,-3.303 -7.273,-7.353 -12.176,-12.15 -0.525,-0.505 -1.058,-1.022 -1.6,-1.55 z m 10.725,16.1 0.101,0.05 h -0.101 v -0.05 z"
id="path28"
style="fill:#dbdbdb" />
<path
d="m 208.35,251.39952 v 0.05 h 0.101 c 0.833,-0.7 1.7,-1.417 2.6,-2.15 0.066,-0.133 0.184,-0.233 0.351,-0.3 0.1,-0.134 0.199,-0.233 0.3,-0.3 l 0.149,-0.15 5.2,5.25 c 0.134,0.167 0.3,0.3 0.5,0.4 l 60.101,60.35 c 1.133,1.066 12.449,12.4 33.949,34 -9.066,-2.467 -16.033,-6.467 -20.899,-12 -4.934,-5.633 -8.283,-9.116 -10.05,-10.45 -1.801,-1.3 -2.817,-1.866 -3.051,-1.699 -0.199,0.267 -0.449,0.483 -0.75,0.649 -1.199,0.9 -2.616,1.384 -4.25,1.45 -1.933,0 -3.6,-0.684 -5,-2.05 l -1.1,-1.101 c -0.233,-0.233 -0.417,-0.416 -0.55,-0.55 l -65.601,-65.75 c 0.067,-0.066 0.15,-0.116 0.25,-0.149 1.134,-0.733 2.267,-1.467 3.4,-2.2 0.233,-0.134 0.483,-0.3 0.75,-0.5 0.134,-0.101 0.3,-0.217 0.5,-0.351 l 3.1,-2.449 z"
id="path30"
style="fill:#c8c8c8" />
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

363
aboutPages/static/site.css Normal file
View File

@@ -0,0 +1,363 @@
body {
text-align: center;
background: url("img/grey.png") repeat;
font-family: 'Roboto', sans-serif;
font-weight: 300;
color: #050505;
word-wrap: break-word;
}
/* roboto-300 - latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url('fonts/roboto-v20-latin_latin-ext-300.eot');
src: local('Roboto Light'), local('Roboto-Light'),
url('fonts/roboto-v20-latin_latin-ext-300.eot?#iefix') format('embedded-opentype'),
url('fonts/roboto-v20-latin_latin-ext-300.woff2') format('woff2'),
url('fonts/roboto-v20-latin_latin-ext-300.woff') format('woff'),
url('fonts/roboto-v20-latin_latin-ext-300.ttf') format('truetype'),
url('fonts/roboto-v20-latin_latin-ext-300.svg#Roboto') format('svg');
}
/* roboto-regular - latin-ext_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('fonts/roboto-v20-latin-ext_latin-regular.eot'); /* IE9 Compat Modes */
src: local('Roboto'), local('Roboto-Regular'),
url('fonts/roboto-v20-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('fonts/roboto-v20-latin-ext_latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('fonts/roboto-v20-latin-ext_latin-regular.woff') format('woff'), /* Modern Browsers */
url('fonts/roboto-v20-latin-ext_latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('fonts/roboto-v20-latin-ext_latin-regular.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-500 - latin-ext_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url('fonts/roboto-v20-latin-ext_latin-500.eot'); /* IE9 Compat Modes */
src: local('Roboto Medium'), local('Roboto-Medium'),
url('fonts/roboto-v20-latin-ext_latin-500.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('fonts/roboto-v20-latin-ext_latin-500.woff2') format('woff2'), /* Super Modern Browsers */
url('fonts/roboto-v20-latin-ext_latin-500.woff') format('woff'), /* Modern Browsers */
url('fonts/roboto-v20-latin-ext_latin-500.ttf') format('truetype'), /* Safari, Android, iOS */
url('fonts/roboto-v20-latin-ext_latin-500.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-mono-300 - latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 300;
src: url('fonts/roboto-mono-v11-latin-300.eot'); /* IE9 Compat Modes */
src: local(''),
url('fonts/roboto-mono-v11-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('fonts/roboto-mono-v11-latin-300.woff2') format('woff2'), /* Super Modern Browsers */
url('fonts/roboto-mono-v11-latin-300.woff') format('woff'), /* Modern Browsers */
url('fonts/roboto-mono-v11-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */
url('fonts/roboto-mono-v11-latin-300.svg#RobotoMono') format('svg'); /* Legacy iOS */
}
/* ubuntu-mono-regular - latin */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
src: url('fonts/ubuntu-mono-v9-latin-regular.eot'); /* IE9 Compat Modes */
src: local('Ubuntu Mono'), local('UbuntuMono-Regular'),
url('fonts/ubuntu-mono-v9-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('fonts/ubuntu-mono-v9-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('fonts/ubuntu-mono-v9-latin-regular.woff') format('woff'), /* Modern Browsers */
url('fonts/ubuntu-mono-v9-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('fonts/ubuntu-mono-v9-latin-regular.svg#UbuntuMono') format('svg'); /* Legacy iOS */
}
h1, h2, h3, h4 {
font-weight: 500;
}
strong {
font-weight: bolder;
}
span.brand {
font-family: "Ubuntu Mono";
}
h1 {
padding-bottom: 0.75em;
padding-bottom: 0.75em;
}
.ui p {
line-height: 1.8em;
}
a, a:visited {
color: #0057B3;
text-decoration: none;
}
hr {
color: #4aa3ff;
}
a.brand {
color: #050505;
}
.usage > h2 a, .usage > h2 a:visited {
color: #050505;
}
.usage h2 div {
display: inline-block;
width: 2.5em;
text-align: center;
}
.usage h2#web,
.usage h2#wkd-as-a-service,
.usage h2#api,
.usage h2#others {
padding-inline-start: 3em;
}
.usage h2 img {
display: inline-block;
height: 2em;
margin-inline-end: .5em;
vertical-align: -30%;
}
h4:target, h3:target, h2:target {
background-color: #ffa;
}
h4 a, h4 a:visited, h3 a, h3 a:visited {
color: #050505;
}
a:hover {
text-decoration: underline;
}
code {
background-color: #ccc;
padding: 0 0.3em;
border-radius: 3px;
box-shadow: 0 2px 6px hsla(0, 0%, 0%, 0.2);
}
code.snippet {
border: 1px solid #aaa;
font-size: 12px;
padding: 1em;
}
.example {
display: flex;
}
.example div {
flex: 1;
min-width: 0;
}
blockquote {
font-family: monospace;
background: #f9f9f9;
border-inline-start: 10px solid #ccc;
margin: 1.5em 10px;
padding: 0.5em 10px;
}
blockquote p {
display: inline;
}
li {
text-align: start;
}
abbr {
text-decoration: none;
color: #444;
}
.search, .upload, .manage {
display: flex;
padding: 0 15%;
}
@media screen and (min-width: 450px) {
.search, .upload, .manage {
display: flex;
padding: 0 15%;
}
}
.manageEmail, .searchTerm, .fileUpload {
flex-grow: 1;
border: 3px solid;
padding: 5px;
height: 20px;
outline: none;
border-radius: 5px 0 0 5px;
}
.rtl .manageEmail, .rtl .searchTerm, .rtl .fileUpload {
border-radius: 0 5px 5px 0;
}
.manageEmail {
border-color: #d93838;
}
.searchTerm, .fileUpload {
border-color: #4aa3ff;
}
.iconButton {
width: 40px;
}
.publishedUid {
margin-left: auto;
margin-right: auto;
width: 65%;
text-align: start;
}
.publishedUid div {
float: right;
}
.rtl .publishedUid div {
float: left;
}
.verificationEmails {
margin-left: auto;
margin-right: auto;
width: 50%;
text-align: start;
}
.verificationEmails ul {
margin-top: 0.3em;
margin-bottom: 0.3em;
}
.verificationEmails li {
line-height: 1.5em;
}
.usage li {
line-height: 1.5em;
}
.button {
height: 36px;
border: 1px solid;
text-align: center;
color: #fff;
border-radius: 5px;
cursor: pointer;
font-size: 20px;
font-family: inherit;
font-weight: bolder;
}
.manageButton {
border-color: #d93838;
background: #d93838;
}
.searchButton, .uploadButton {
border-color: #4aa3ff;
background: #4aa3ff;
}
.searchButton, .manageButton, .uploadButton {
border-radius: 0 5px 5px 0;
}
.rtl .searchButton, .rtl .manageButton, .rtl .uploadButton {
border-radius: 5px 0 0 5px;
}
.button img, .button svg {
vertical-align: middle;
}
.card {
position: relative;
display: inline-block;
margin-top: 4vh;
padding: 20px 30px 50px;
background-color: #fff;
border-top: 5px solid #4aa3ff;
box-shadow: 0 2px 6px hsla(0, 0%, 0%, 0.2);
width: 80vw;
max-width: 800px;
}
/* This is used to enlarge the card, but is also used to constrain the
flow of text in /about. */
.card > .about {
text-align: start;
}
span.fingerprint {
font-family: "Ubuntu Mono";
}
span.email {
font-family: "Ubuntu Mono";
white-space: nowrap;
}
.debug_link {
position: absolute;
right: 0px;
left: 0px;
top: 0px;
margin: 10px;
font-size: 12px;
text-align: end;
}
.debug_link a {
color: transparent;
}
.debug_link a:hover {
color: #bbb;
}
.attribution {
position: fixed;
z-index: -1;
bottom: 0;
right: 0;
left: 0;
text-align: end;
font-size: 12px;
color: #bbb;
margin: 10px 20px;
}
.attribution a {
color: #ccc;
}
input.textbutton {
padding: 0px;
border: none;
background: none;
margin: 0px;
cursor: pointer;
color: blue;
font-family: inherit;
font-size: inherit;
}

View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="{{ get_url(path='/site.css') }}?v=19" type="text/css">
<link rel="alternate" href="{{ get_url(path='/atom.xml') }}" type="application/atom+xml" title="keys.openpgp.org newsfeed" />
<title>{{config.title}}</title>
</head>
<body>
<div class="card">
<h1><a class="brand" href="/">{{config.title}}</a></h1>
{% block content %}{% endblock content %}
<div class="spacer"></div>
</div>
<div class="attribution">
<p>Background image retrieved from <a href="https://www.toptal.com/designers/subtlepatterns/subtle-grey/">Subtle Patterns</a> under CC BY-SA 3.0</p>
</div>
</body>

View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block content %}
<div class="about {%- if page %} {{ page.slug }}{%- endif -%}">
{% include "partials/menu.html" %}
{% if section.content %}{{ section.content | safe }}{% endif %}
{% for post in section.pages %}
{% set post_date = post.date | date(format="%Y-%m-%d") %}
{% set post_id = post_date ~ "-" ~ post.slug %}
<h2 id="{{ post_id }}">
<div style="float: right; font-size: small; line-height: 2em;">{{post.date}} 📅</div>
<a style="color: black;" href="{{ section.permalink | safe}}#{{ post_id | safe}}">{{ post.title }}</a>
</h2>
{{ post.content | safe }}
{% if not loop.last %}
<hr style="margin-top: 2em; margin-bottom: 2em;">
{% endif %}
{% endfor %}
</div>
{% endblock content %}

View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="about {%- if page %} {{ page.slug }}{%- endif -%}">
{% include "partials/menu.html" %}
{% if page %}{{ page.content | safe }}{% elif section %}{{ section.content | safe }}{% endif %}
</div>
{% endblock content %}

View File

@@ -0,0 +1,15 @@
<center><h2>
{%- for item in config.extra.menu -%}
{%- if item is ending_with("_index.md") -%}
{%- set item = get_section(path=item, metadata_only=true) -%}
{%- else -%}
{%- set item = get_page(path=item) -%}
{%- endif -%}
{% if not loop.first %} | {% endif -%}
{% if item.path == current_path -%}
{{ item.title }}
{%- else -%}
<a href="{{ item.permalink | safe }}">{{ item.title }}</a>
{%- endif %}
{% endfor %}</h2>
</center>

View File

@@ -0,0 +1 @@
<span class="brand">{{ name | default(value=config.title) }}</span>

View File

@@ -0,0 +1 @@
<img src="{{ get_url(path=path) }}" alt="{{ alt }}">

View File

@@ -0,0 +1 @@
<img src="{{ src }}" style="height: {{ height }};">

View File

@@ -1,9 +1,6 @@
extern crate vergen;
use vergen::{generate_cargo_keys, ConstantsFlags};
use vergen::{ConstantsFlags, generate_cargo_keys};
fn main() {
// Generate the 'cargo:' key output
generate_cargo_keys(ConstantsFlags::all())
.expect("Unable to generate the cargo keys!");
generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!");
}

3
clippy.toml Normal file
View File

@@ -0,0 +1,3 @@
msrv = "1.86"
too-many-arguments-threshold = 10

View File

@@ -1,70 +0,0 @@
#!/usr/bin/env zsh
set -e
[[ $# == 4 || $# == 5 ]] || { echo "Usage: $0 keys-internal-dir keys-external-dir encryption-key backup-dir [date]" >&2; exit 1; }
local keys_internal_dir=$1
local keys_external_dir=$2
local encryption_key=$3
local backup_dir=$4
# backupdate in format YYYY-MM-DD
local backupdate=$5
[[ -d $keys_internal_dir ]] || { echo "Missing dir $keys_internal_dir" >&2; exit 1; }
[[ -d $keys_internal_dir/log ]] || { echo "Missing dir $keys_internal_dir/log" >&2; exit 1; }
[[ -d $keys_external_dir ]] || { echo "Missing dir $keys_external_dir" >&2; exit 1; }
[[ -d $keys_external_dir/pub ]] || { echo "Missing dir $keys_external_dir/pub" >&2; exit 1; }
[[ -f $encryption_key ]] || { echo "Missing file $encryption_key" >&2; exit 1; }
[[ -d $backup_dir ]] || { echo "Missing dir $backup_dir" >&2; exit 1; }
if [[ -z $backupdate ]]; then
# for EPOCHSECONDS
zmodload zsh/datetime
backupdate="$(date --date=@$(( EPOCHSECONDS - 24*60*60 )) +'%Y-%m-%d')"
fi
local log_file="$keys_internal_dir/log/$backupdate"
[[ -f $log_file ]] || { echo "Missing dir $log_file" >&2; exit 1; }
local tempdir=$(mktemp -d)
trap "rm -rf ${(q)tempdir}" EXIT
local keylist_file=$tempdir/keylist
integer count=0
cat $log_file | cut -d' ' -f2 | sort -u | while read -r fp; do
key_file=${fp[1,2]}/${fp[3,4]}/${fp[5,$]}
[[ -f $keys_external_dir/pub/$key_file ]] || { echo "Missing file $key_file" >&2; exit 1; }
echo -E - $key_file
count+=1
done > $keylist_file
local backup_file_unencrypted=$tempdir/$backupdate.tar.gz
local backup_file_encrypted=$tempdir/$backupdate.tar.gz.pgp
tar \
--create \
--gzip \
--file $backup_file_unencrypted \
--verbatim-files-from \
--directory $keys_external_dir/pub \
--files-from $keylist_file
GNUPGHOME=$tempdir gpg \
--quiet \
--no-keyring \
--compress-level 0 \
--recipient-file $encryption_key \
--output $backup_file_encrypted \
--encrypt $backup_file_unencrypted
backup_file=$backup_dir/$backupdate.tar.gz.pgp
mv $backup_file_encrypted $backup_file
sha256sum="$(cd $backup_dir; sha256sum $backupdate.tar.gz.pgp)"
echo $sha256sum >> $backup_dir/SHA256SUM
echo "finished backup for $backupdate, total keys $count"
ls -l $backup_file
echo $sha256sum

View File

@@ -1,13 +0,0 @@
[Unit]
Description=Hagrid Verifying Keyserver
After=network.target
[Service]
ExecStart=/opt/hagrid/target/release/hagrid /opt/hagrid/dist -D %i -F hagrid@%i
WorkingDirectory=/opt/hagrid
User=hagrid
Group=hagrid
StandardOutput=syslog
[Install]
WantedBy=multi-user.target

View File

@@ -2,27 +2,31 @@
name = "hagrid-database"
version = "0.1.0"
authors = ["Kai Michaelis <kai@sequoia-pgp.org>"]
edition = "2024"
[dependencies]
anyhow = "1"
sequoia-openpgp = { version = "1.3", default-features = false, features = ["crypto-nettle"] }
multipart = "0"
log = "0"
rand = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
time = "0.1"
tempfile = "3.0"
url = "1.6"
hex = "0.3"
base64 = "0.10"
pathdiff = "0.1"
idna = "0.1"
fs2 = "0.4"
walkdir = "2.2"
chrono = "0.4"
zbase32 = "0.1.2"
anyhow = { workspace = true }
sequoia-openpgp = { workspace = true, features = ["crypto-openssl"] }
log = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_derive = { workspace = true }
serde_json = { workspace = true }
time = { workspace = true }
tempfile = { workspace = true }
url = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
pathdiff = { workspace = true }
idna = { workspace = true }
fs2 = { workspace = true }
walkdir = { workspace = true }
chrono = { workspace = true }
zbase32 = { workspace = true }
r2d2 = { workspace = true }
r2d2_sqlite = { workspace = true }
rusqlite = { workspace = true, features = ["trace"] }
self_cell = { workspace = true }
[lib]
name = "hagrid_database"

View File

@@ -1,939 +0,0 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fs::{OpenOptions, File, create_dir_all, read_link, remove_file, rename, set_permissions, Permissions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::os::unix::fs::PermissionsExt;
use tempfile;
use url::form_urlencoded;
use pathdiff::diff_paths;
use std::time::SystemTime;
use {Database, Query};
use types::{Email, Fingerprint, KeyID};
use sync::FlockMutexGuard;
use Result;
use wkd;
use tempfile::NamedTempFile;
use openpgp::Cert;
use openpgp_utils::POLICY;
pub struct Filesystem {
tmp_dir: PathBuf,
keys_internal_dir: PathBuf,
keys_external_dir: PathBuf,
keys_dir_full: PathBuf,
keys_dir_quarantined: PathBuf,
keys_dir_published: PathBuf,
keys_dir_published_wkd: PathBuf,
keys_dir_log: PathBuf,
links_dir_by_fingerprint: PathBuf,
links_dir_by_keyid: PathBuf,
links_dir_wkd_by_email: PathBuf,
links_dir_by_email: PathBuf,
dry_run: bool,
}
/// Returns the given path, ensuring that the parent directory exists.
///
/// Use this on paths returned by .path_to_* before creating the
/// object.
fn ensure_parent(path: &Path) -> Result<&Path> {
let parent = path.parent().unwrap();
create_dir_all(parent)?;
Ok(path)
}
impl Filesystem {
pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self> {
let base_dir: PathBuf = base_dir.into();
let keys_dir = base_dir.join("keys");
let tmp_dir = base_dir.join("tmp");
Self::new(&keys_dir, &keys_dir, tmp_dir)
}
pub fn new(
keys_internal_dir: impl Into<PathBuf>,
keys_external_dir: impl Into<PathBuf>,
tmp_dir: impl Into<PathBuf>,
) -> Result<Self> {
Self::new_internal(keys_internal_dir, keys_external_dir, tmp_dir, false)
}
pub fn new_internal(
keys_internal_dir: impl Into<PathBuf>,
keys_external_dir: impl Into<PathBuf>,
tmp_dir: impl Into<PathBuf>,
dry_run: bool,
) -> Result<Self> {
let tmp_dir = tmp_dir.into();
create_dir_all(&tmp_dir)?;
let keys_internal_dir: PathBuf = keys_internal_dir.into();
let keys_external_dir: PathBuf = keys_external_dir.into();
let keys_dir_full = keys_internal_dir.join("full");
let keys_dir_quarantined = keys_internal_dir.join("quarantined");
let keys_dir_log = keys_internal_dir.join("log");
let keys_dir_published = keys_external_dir.join("pub");
let keys_dir_published_wkd = keys_external_dir.join("wkd");
create_dir_all(&keys_dir_full)?;
create_dir_all(&keys_dir_quarantined)?;
create_dir_all(&keys_dir_published)?;
create_dir_all(&keys_dir_published_wkd)?;
create_dir_all(&keys_dir_log)?;
let links_dir = keys_external_dir.join("links");
let links_dir_by_keyid = links_dir.join("by-keyid");
let links_dir_by_fingerprint = links_dir.join("by-fpr");
let links_dir_by_email = links_dir.join("by-email");
let links_dir_wkd_by_email = links_dir.join("wkd");
create_dir_all(&links_dir_by_keyid)?;
create_dir_all(&links_dir_by_fingerprint)?;
create_dir_all(&links_dir_by_email)?;
create_dir_all(&links_dir_wkd_by_email)?;
info!("Opened filesystem database.");
info!("keys_internal_dir: '{}'", keys_internal_dir.display());
info!("keys_external_dir: '{}'", keys_external_dir.display());
info!("tmp_dir: '{}'", tmp_dir.display());
Ok(Filesystem {
keys_internal_dir,
keys_external_dir,
tmp_dir,
keys_dir_full,
keys_dir_published,
keys_dir_published_wkd,
keys_dir_quarantined,
keys_dir_log,
links_dir_by_keyid,
links_dir_by_fingerprint,
links_dir_by_email,
links_dir_wkd_by_email,
dry_run,
})
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_full(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_full.join(path_split(&hex))
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_quarantined(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_quarantined.join(&hex)
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_published(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_published.join(path_split(&hex))
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_published_wkd(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_published_wkd.join(path_split(&hex))
}
/// Returns the path to the given KeyID.
fn link_by_keyid(&self, keyid: &KeyID) -> PathBuf {
let hex = keyid.to_string();
self.links_dir_by_keyid.join(path_split(&hex))
}
/// Returns the path to the given Fingerprint.
fn link_by_fingerprint(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.links_dir_by_fingerprint.join(path_split(&hex))
}
/// Returns the path to the given Email.
fn link_by_email(&self, email: &Email) -> PathBuf {
let email = form_urlencoded::byte_serialize(email.as_str().as_bytes())
.collect::<String>();
self.links_dir_by_email.join(path_split(&email))
}
/// Returns the WKD path to the given Email.
fn link_wkd_by_email(&self, email: &Email) -> PathBuf {
let (encoded_local_part, domain) = wkd::encode_wkd(email.as_str()).unwrap();
let encoded_domain = form_urlencoded::byte_serialize(domain.as_bytes())
.collect::<PathBuf>();
[
&self.links_dir_wkd_by_email,
&encoded_domain,
&path_split(&encoded_local_part)
].iter().collect()
}
fn read_from_path(&self, path: &Path, allow_internal: bool) -> Option<String> {
use std::fs;
if !path.starts_with(&self.keys_external_dir) &&
!(allow_internal && path.starts_with(&self.keys_internal_dir)) {
panic!("Attempted to access file outside expected dirs!");
}
if path.exists() {
fs::read_to_string(path).ok()
} else {
None
}
}
fn read_from_path_bytes(&self, path: &Path, allow_internal: bool) -> Option<Vec<u8>> {
use std::fs;
if !path.starts_with(&self.keys_external_dir) &&
!(allow_internal && path.starts_with(&self.keys_internal_dir)) {
panic!("Attempted to access file outside expected dirs!");
}
if path.exists() {
fs::read(path).ok()
} else {
None
}
}
/// Returns the Fingerprint the given path is pointing to.
pub fn path_to_fingerprint(path: &Path) -> Option<Fingerprint> {
use std::str::FromStr;
let merged = path_merge(path);
Fingerprint::from_str(&merged).ok()
}
/// Returns the KeyID the given path is pointing to.
fn path_to_keyid(path: &Path) -> Option<KeyID> {
use std::str::FromStr;
let merged = path_merge(path);
KeyID::from_str(&merged).ok()
}
/// Returns the Email the given path is pointing to.
fn path_to_email(path: &Path) -> Option<Email> {
use std::str::FromStr;
let merged = path_merge(path);
let decoded = form_urlencoded::parse(merged.as_bytes()).next()?.0;
Email::from_str(&decoded).ok()
}
/// Returns the backing primary key fingerprint for any key path.
pub fn path_to_primary(path: &Path) -> Option<Fingerprint> {
use std::fs;
let typ = fs::symlink_metadata(&path).ok()?.file_type();
if typ.is_symlink() {
let path = read_link(path).ok()?;
Filesystem::path_to_fingerprint(&path)
} else {
Filesystem::path_to_fingerprint(path)
}
}
fn link_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let path = self.fingerprint_to_path_published(fpr);
let link = self.link_by_email(&email);
let target = diff_paths(&path, link.parent().unwrap()).unwrap();
if link == target {
return Ok(());
}
symlink(&target, ensure_parent(&link)?)
}
fn link_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let path = self.fingerprint_to_path_published_wkd(fpr);
let link = self.link_wkd_by_email(&email);
let target = diff_paths(&path, link.parent().unwrap()).unwrap();
if link == target {
return Ok(());
}
symlink(&target, ensure_parent(&link)?)
}
fn unlink_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let link = self.link_by_email(&email);
let expected = diff_paths(
&self.fingerprint_to_path_published(fpr),
link.parent().unwrap()
).unwrap();
symlink_unlink_with_check(&link, &expected)
}
fn unlink_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let link = self.link_wkd_by_email(&email);
let expected = diff_paths(
&self.fingerprint_to_path_published_wkd(fpr),
link.parent().unwrap()
).unwrap();
symlink_unlink_with_check(&link, &expected)
}
fn open_logfile(&self, file_name: &str) -> Result<File> {
let file_path = self.keys_dir_log.join(file_name);
Ok(OpenOptions::new()
.create(true)
.append(true)
.open(file_path)?)
}
fn perform_checks(
&self,
checks_dir: &Path,
tpks: &mut HashMap<Fingerprint, Cert>,
check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<()>,
) -> Result<()> {
use walkdir::WalkDir;
use std::fs;
for entry in WalkDir::new(checks_dir) {
let entry = entry?;
let path = entry.path();
let typ = fs::symlink_metadata(&path)?.file_type();
if typ.is_dir() {
continue;
}
// Compute the corresponding primary fingerprint just
// by looking at the paths.
let primary_fp = Filesystem::path_to_primary(path)
.ok_or_else(
|| format_err!("Malformed path: {:?}",
path.read_link().unwrap()))?;
// Load into cache.
if ! tpks.contains_key(&primary_fp) {
tpks.insert(
primary_fp.clone(),
self.lookup(&Query::ByFingerprint(primary_fp.clone()))
?.ok_or_else(
|| format_err!("No Cert with fingerprint {:?}",
primary_fp))?);
}
let tpk = tpks.get(&primary_fp)
.ok_or_else(
|| format_err!("Broken symlink {:?}: No such Key {}",
path, primary_fp))?;
check(&path, &tpk, &primary_fp)?;
}
Ok(())
}
}
// Like `symlink`, but instead of failing if `symlink_name` already
// exists, atomically update `symlink_name` to have `symlink_content`.
fn symlink(symlink_content: &Path, symlink_name: &Path) -> Result<()> {
use std::os::unix::fs::{symlink};
let symlink_dir = ensure_parent(symlink_name)?.parent().unwrap();
let tmp_dir = tempfile::Builder::new()
.prefix("link")
.rand_bytes(16)
.tempdir_in(symlink_dir)?;
let symlink_name_tmp = tmp_dir.path().join("link");
symlink(&symlink_content, &symlink_name_tmp)?;
rename(&symlink_name_tmp, &symlink_name)?;
Ok(())
}
fn symlink_unlink_with_check(link: &Path, expected: &Path) -> Result<()> {
if let Ok(target) = read_link(&link) {
if target == expected {
remove_file(link)?;
}
}
Ok(())
}
impl Database for Filesystem {
type MutexGuard = FlockMutexGuard;
fn lock(&self) -> Result<Self::MutexGuard> {
FlockMutexGuard::lock(&self.keys_internal_dir)
}
fn write_to_temp(&self, content: &[u8]) -> Result<NamedTempFile> {
let mut tempfile = tempfile::Builder::new()
.prefix("key")
.rand_bytes(16)
.tempfile_in(&self.tmp_dir)?;
tempfile.write_all(content).unwrap();
Ok(tempfile)
}
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()> {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let fingerprint_line = format!("{:010} {}\n", timestamp, fpr_primary.to_string());
self.open_logfile(filename)?
.write_all(fingerprint_line.as_bytes())?;
Ok(())
}
fn move_tmp_to_full(&self, file: NamedTempFile, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
set_permissions(file.path(), Permissions::from_mode(0o640))?;
let target = self.fingerprint_to_path_full(fpr);
file.persist(ensure_parent(&target)?)?;
Ok(())
}
fn move_tmp_to_published(&self, file: NamedTempFile, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
set_permissions(file.path(), Permissions::from_mode(0o644))?;
let target = self.fingerprint_to_path_published(fpr);
file.persist(ensure_parent(&target)?)?;
Ok(())
}
fn move_tmp_to_published_wkd(&self, file: Option<NamedTempFile>, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
let target = self.fingerprint_to_path_published_wkd(fpr);
if let Some(file) = file {
set_permissions(file.path(), Permissions::from_mode(0o644))?;
file.persist(ensure_parent(&target)?)?;
} else if target.exists() {
remove_file(target)?;
}
Ok(())
}
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()> {
let mut tempfile = tempfile::Builder::new()
.prefix("key")
.rand_bytes(16)
.tempfile_in(&self.tmp_dir)?;
tempfile.write_all(content).unwrap();
let target = self.fingerprint_to_path_quarantined(fpr);
tempfile.persist(ensure_parent(&target)?)?;
Ok(())
}
fn check_link_fpr(&self, fpr: &Fingerprint, fpr_target: &Fingerprint) -> Result<Option<Fingerprint>> {
let link_keyid = self.link_by_keyid(&fpr.into());
let link_fpr = self.link_by_fingerprint(&fpr);
let path_published = self.fingerprint_to_path_published(fpr_target);
if let Ok(link_fpr_target) = link_fpr.canonicalize() {
if !link_fpr_target.ends_with(&path_published) {
info!("Fingerprint points to different key for {} (expected {:?} to be suffix of {:?})",
fpr, &path_published, &link_fpr_target);
Err(anyhow!(format!("Fingerprint collision for key {}", fpr)))?;
}
}
if let Ok(link_keyid_target) = link_keyid.canonicalize() {
if !link_keyid_target.ends_with(&path_published) {
info!("KeyID points to different key for {} (expected {:?} to be suffix of {:?})",
fpr, &path_published, &link_keyid_target);
Err(anyhow!(format!("KeyID collision for key {}", fpr)))?;
}
}
if !link_fpr.exists() || !link_keyid.exists() {
Ok(Some(fpr.clone()))
} else {
Ok(None)
}
}
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint> {
use super::Query::*;
let path = match term {
ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
ByKeyID(ref keyid) => self.link_by_keyid(keyid),
ByEmail(ref email) => self.link_by_email(email),
_ => return None
};
path.read_link()
.ok()
.and_then(|link_path| Filesystem::path_to_fingerprint(&link_path))
}
/// Gets the path to the underlying file, if any.
fn lookup_path(&self, term: &Query) -> Option<PathBuf> {
use super::Query::*;
let path = match term {
ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
ByKeyID(ref keyid) => self.link_by_keyid(keyid),
ByEmail(ref email) => self.link_by_email(email),
_ => return None
};
if path.exists() {
let x = diff_paths(&path, &self.keys_external_dir).expect("related paths");
Some(x)
} else {
None
}
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
self.link_email_vks(email, fpr)?;
self.link_email_wkd(email, fpr)?;
Ok(())
}
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
self.unlink_email_vks(email, fpr)?;
self.unlink_email_wkd(email, fpr)?;
Ok(())
}
fn link_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
let link_fpr = self.link_by_fingerprint(from);
let link_keyid = self.link_by_keyid(&from.into());
let target = diff_paths(&self.fingerprint_to_path_published(primary_fpr),
link_fpr.parent().unwrap()).unwrap();
symlink(&target, ensure_parent(&link_fpr)?)?;
symlink(&target, ensure_parent(&link_keyid)?)
}
fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
let link_fpr = self.link_by_fingerprint(from);
let link_keyid = self.link_by_keyid(&from.into());
let expected = diff_paths(&self.fingerprint_to_path_published(primary_fpr),
link_fpr.parent().unwrap()).unwrap();
match read_link(&link_fpr) {
Ok(target) => {
if target == expected {
remove_file(&link_fpr)?;
}
}
Err(_) => {}
}
match read_link(&link_keyid) {
Ok(target) => {
if target == expected {
remove_file(link_keyid)?;
}
}
Err(_) => {}
}
Ok(())
}
// XXX: slow
fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String> {
let path = self.fingerprint_to_path_full(fpr);
self.read_from_path(&path, true)
}
// XXX: slow
fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String> {
let path = self.fingerprint_to_path_published(fpr);
self.read_from_path(&path, false)
}
// XXX: slow
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String> {
let path = self.link_by_fingerprint(fpr);
self.read_from_path(&path, false)
}
// XXX: slow
fn by_email(&self, email: &Email) -> Option<String> {
let path = self.link_by_email(&email);
self.read_from_path(&path, false)
}
// XXX: slow
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>> {
let path = self.link_wkd_by_email(&email);
self.read_from_path_bytes(&path, false)
}
// XXX: slow
fn by_kid(&self, kid: &KeyID) -> Option<String> {
let path = self.link_by_keyid(kid);
self.read_from_path(&path, false)
}
/// Checks the database for consistency.
///
/// Note that this operation may take a long time, and is
/// generally only useful for testing.
fn check_consistency(&self) -> Result<()> {
// A cache of all Certs, for quick lookups.
let mut tpks = HashMap::new();
self.perform_checks(&self.keys_dir_published, &mut tpks,
|path, _, primary_fp| {
// The KeyID corresponding with this path.
let fp = Filesystem::path_to_fingerprint(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
if fp != *primary_fp {
return Err(format_err!(
"{:?} points to the wrong Cert, expected {} \
but found {}",
path, fp, primary_fp));
}
Ok(())
}
)?;
self.perform_checks(&self.keys_dir_published, &mut tpks,
|_, tpk, primary_fp| {
// check that certificate exists in published wkd path
let path_wkd = self.fingerprint_to_path_published_wkd(&primary_fp);
let should_wkd_exist = tpk.userids().next().is_some();
if should_wkd_exist && !path_wkd.exists() {
return Err(format_err!("Missing wkd for fp {}", primary_fp));
};
if !should_wkd_exist && path_wkd.exists() {
return Err(format_err!("Incorrectly present wkd for fp {}", primary_fp));
};
Ok(())
}
)?;
// check that all subkeys are linked
self.perform_checks(&self.keys_dir_published, &mut tpks,
|_, tpk, primary_fp| {
let policy = &POLICY;
let fingerprints = tpk
.keys()
.with_policy(policy, None)
.for_certification()
.for_signing()
.map(|amalgamation| amalgamation.key().fingerprint())
.map(|fpr| Fingerprint::try_from(fpr))
.flatten();
for fpr in fingerprints {
if let Some(missing_fpr) = self.check_link_fpr(&fpr, &primary_fp)? {
return Err(format_err!(
"Missing link to key {} for sub {}", primary_fp, missing_fpr));
}
}
Ok(())
}
)?;
// check that all published uids are linked
self.perform_checks(&self.keys_dir_published, &mut tpks,
|_, tpk, primary_fp| {
let emails = tpk
.userids()
.map(|binding| binding.userid().clone())
.map(|userid| Email::try_from(&userid).unwrap());
for email in emails {
let email_path = self.link_by_email(&email);
if !email_path.exists() {
return Err(format_err!(
"Missing link to key {} for email {}", primary_fp, email));
}
let email_wkd_path = self.link_wkd_by_email(&email);
if !email_wkd_path.exists() {
return Err(format_err!(
"Missing wkd link to key {} for email {}", primary_fp, email));
}
}
Ok(())
}
)?;
self.perform_checks(&self.links_dir_by_fingerprint, &mut tpks,
|path, tpk, _| {
// The KeyID corresponding with this path.
let id = Filesystem::path_to_keyid(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let found = tpk.keys()
.map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap())
.any(|key_fp| key_fp == id);
if ! found {
return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \
contain the (sub)key {}", path, id));
}
Ok(())
}
)?;
self.perform_checks(&self.links_dir_by_keyid, &mut tpks,
|path, tpk, _| {
// The KeyID corresponding with this path.
let id = Filesystem::path_to_keyid(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let found = tpk.keys()
.map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap())
.any(|key_fp| key_fp == id);
if ! found {
return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \
contain the (sub)key {}", path, id));
}
Ok(())
}
)?;
self.perform_checks(&self.links_dir_by_email, &mut tpks,
|path, tpk, _| {
// The Email corresponding with this path.
let email = Filesystem::path_to_email(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let mut found = false;
for uidb in tpk.userids() {
if Email::try_from(uidb.userid()).unwrap() == email
{
found = true;
break;
}
}
if ! found {
return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \
contain the email {}", path, email));
}
Ok(())
})?;
Ok(())
}
}
fn path_split(path: &str) -> PathBuf {
if path.len() > 4 {
[&path[..2], &path[2..4], &path[4..]].iter().collect()
} else {
path.into()
}
}
fn path_merge(path: &Path) -> String {
let comps = path.iter().rev().take(3).collect::<Vec<_>>().into_iter().rev();
let comps: Vec<_> = comps.map(|os| os.to_string_lossy()).collect();
comps.join("")
}
#[cfg(test)]
mod tests {
use super::*;
use test;
use openpgp::cert::CertBuilder;
use tempfile::TempDir;
#[test]
fn init() {
let tmpdir = TempDir::new().unwrap();
let _ = Filesystem::new_from_base(tmpdir.path()).unwrap();
}
fn open_db() -> (TempDir, Filesystem, PathBuf) {
let tmpdir = TempDir::new().unwrap();
let db = Filesystem::new_from_base(tmpdir.path()).unwrap();
let log_path = db.keys_dir_log.join(db.get_current_log_filename());
(tmpdir, db, log_path)
}
#[test]
fn new() {
let (_tmp_dir, db, _log_path) = open_db();
let k1 = CertBuilder::new().add_userid("a@invalid.example.org")
.generate().unwrap().0;
let k2 = CertBuilder::new().add_userid("b@invalid.example.org")
.generate().unwrap().0;
let k3 = CertBuilder::new().add_userid("c@invalid.example.org")
.generate().unwrap().0;
assert!(db.merge(k1).unwrap().into_tpk_status().email_status.len() > 0);
assert!(db.merge(k2.clone()).unwrap().into_tpk_status().email_status.len() > 0);
assert!(!db.merge(k2).unwrap().into_tpk_status().email_status.len() > 0);
assert!(db.merge(k3.clone()).unwrap().into_tpk_status().email_status.len() > 0);
assert!(!db.merge(k3.clone()).unwrap().into_tpk_status().email_status.len() > 0);
assert!(!db.merge(k3).unwrap().into_tpk_status().email_status.len() > 0);
}
#[test]
fn uid_verification() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_verification(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_deletion() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_deletion(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn subkey_lookup() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_subkey_lookup(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn kid_lookup() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_kid_lookup(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn upload_revoked_tpk() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_upload_revoked_tpk(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_revocation() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_revocation(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn regenerate() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_regenerate(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn key_reupload() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_reupload(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_replacement() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_replacement(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_unlinking() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_unlink_uid(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_1() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_1(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_2() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_2(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_3() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_3(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_4() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_4(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn no_selfsig() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_no_selfsig(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn bad_uids() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_bad_uids(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn reverse_fingerprint_to_path() {
let tmpdir = TempDir::new().unwrap();
let db = Filesystem::new_from_base(tmpdir.path()).unwrap();
let fp: Fingerprint =
"CBCD8F030588653EEDD7E2659B7DD433F254904A".parse().unwrap();
assert_eq!(Filesystem::path_to_fingerprint(&db.link_by_fingerprint(&fp)),
Some(fp.clone()));
db.check_consistency().expect("inconsistent database");
}
#[test]
fn attested_key_signatures() -> Result<()> {
let (_tmp_dir, mut db, log_path) = open_db();
test::attested_key_signatures(&mut db, &log_path)?;
db.check_consistency()?;
Ok(())
}
}

View File

@@ -1,56 +1,31 @@
#![recursion_limit = "1024"]
use std::convert::TryFrom;
use std::path::PathBuf;
use std::str::FromStr;
use openpgp::serialize::SerializeInto;
use sequoia_openpgp::{
Cert, packet::UserID, parse::Parse, serialize::SerializeInto, types::KeyFlags,
};
use chrono::prelude::Utc;
#[macro_use]
extern crate anyhow;
use anyhow::Result;
extern crate fs2;
extern crate idna;
#[macro_use]
extern crate log;
extern crate pathdiff;
extern crate rand;
extern crate serde;
extern crate serde_json;
extern crate tempfile;
extern crate time;
extern crate url;
extern crate hex;
extern crate walkdir;
extern crate chrono;
extern crate zbase32;
use tempfile::NamedTempFile;
extern crate sequoia_openpgp as openpgp;
use openpgp::{
Cert,
packet::UserID,
parse::Parse,
types::KeyFlags,
};
use anyhow::anyhow;
use log::{error, info};
pub mod types;
use types::{Email, Fingerprint, KeyID};
pub mod wkd;
pub mod sync;
pub mod wkd;
mod fs;
pub use self::fs::Filesystem as KeyDatabase;
mod sqlite;
pub use crate::sqlite::Sqlite;
mod stateful_tokens;
pub use stateful_tokens::StatefulTokens;
mod openpgp_utils;
use openpgp_utils::{tpk_filter_alive_emails, tpk_to_string, tpk_clean, is_status_revoked, POLICY};
use openpgp_utils::{POLICY, is_status_revoked, tpk_clean, tpk_filter_alive_emails, tpk_to_string};
#[cfg(test)]
mod test;
@@ -67,22 +42,18 @@ pub enum Query {
impl Query {
pub fn is_invalid(&self) -> bool {
match self {
Query::Invalid() => true,
Query::InvalidShort() => true,
_ => false,
}
matches!(self, Query::Invalid() | Query::InvalidShort())
}
}
impl FromStr for Query {
type Err = anyhow::Error;
fn from_str(term: &str) -> Result<Self> {
fn from_str(term: &str) -> anyhow::Result<Self> {
use self::Query::*;
let looks_like_short_key_id = !term.contains('@') &&
(term.starts_with("0x") && term.len() < 16 || term.len() == 8);
let looks_like_short_key_id =
!term.contains('@') && (term.starts_with("0x") && term.len() < 16 || term.len() == 8);
if looks_like_short_key_id {
Ok(InvalidShort())
} else if let Ok(fp) = Fingerprint::from_str(term) {
@@ -97,7 +68,7 @@ impl FromStr for Query {
}
}
#[derive(Debug,PartialEq,Eq,PartialOrd,Ord)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum EmailAddressStatus {
Published,
NotPublished,
@@ -118,12 +89,20 @@ impl ImportResult {
ImportResult::Unchanged(status) => status,
}
}
pub fn as_tpk_status(&self) -> &TpkStatus {
match self {
ImportResult::New(status) => status,
ImportResult::Updated(status) => status,
ImportResult::Unchanged(status) => status,
}
}
}
#[derive(Debug,PartialEq)]
#[derive(Debug, PartialEq)]
pub struct TpkStatus {
pub is_revoked: bool,
pub email_status: Vec<(Email,EmailAddressStatus)>,
pub email_status: Vec<(Email, EmailAddressStatus)>,
pub unparsed_uids: usize,
}
@@ -132,23 +111,73 @@ pub enum RegenerateResult {
Unchanged,
}
pub trait Database: Sync + Send {
type MutexGuard;
pub trait DatabaseTransaction<'a> {
type TempCert;
fn commit(self) -> anyhow::Result<()>;
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()>;
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()>;
fn link_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> anyhow::Result<()>;
fn unlink_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> anyhow::Result<()>;
fn write_to_temp(&self, content: &[u8]) -> anyhow::Result<Self::TempCert>;
fn move_tmp_to_full(&self, content: Self::TempCert, fpr: &Fingerprint) -> anyhow::Result<()>;
fn move_tmp_to_published(
&self,
content: Self::TempCert,
fpr: &Fingerprint,
) -> anyhow::Result<()>;
fn move_tmp_to_published_wkd(
&self,
content: Option<Self::TempCert>,
fpr: &Fingerprint,
) -> anyhow::Result<()>;
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> anyhow::Result<()>;
}
pub trait Database<'a>: Sync + Send {
type Transaction: DatabaseTransaction<'a>;
/// Lock the DB for a complex update.
///
/// All basic write operations are atomic so we don't need to lock
/// read operations to ensure that we return something sane.
fn lock(&self) -> Result<Self::MutexGuard>;
fn transaction(&'a self) -> anyhow::Result<Self::Transaction>;
/// Queries the database using Fingerprint, KeyID, or
/// email-address, returning the primary fingerprint.
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint>;
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn by_kid(&self, kid: &KeyID) -> Option<String>;
fn by_email(&self, email: &Email) -> Option<String>;
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>>;
fn by_domain_and_hash_wkd(&self, domain: &str, hash: &str) -> Option<Vec<u8>>;
fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String>;
fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn get_last_log_entry(&self) -> anyhow::Result<Fingerprint>;
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> anyhow::Result<()>;
fn check_link_fpr(
&self,
fpr: &Fingerprint,
target: &Fingerprint,
) -> anyhow::Result<Option<Fingerprint>>;
fn check_consistency(&self) -> anyhow::Result<()>;
/// Queries the database using Fingerprint, KeyID, or
/// email-address.
fn lookup(&self, term: &Query) -> Result<Option<Cert>> {
fn lookup(&self, term: &Query) -> anyhow::Result<Option<Cert>> {
use self::Query::*;
let armored = match term {
ByFingerprint(ref fp) => self.by_fpr(fp),
ByKeyID(ref keyid) => self.by_kid(keyid),
ByEmail(ref email) => self.by_email(&email),
ByFingerprint(fp) => self.by_fpr(fp),
ByKeyID(keyid) => self.by_kid(keyid),
ByEmail(email) => self.by_email(email),
_ => None,
};
@@ -158,27 +187,6 @@ pub trait Database: Sync + Send {
}
}
/// Queries the database using Fingerprint, KeyID, or
/// email-address, returning the primary fingerprint.
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint>;
/// Gets the path to the underlying file, if any.
fn lookup_path(&self, term: &Query) -> Option<PathBuf> {
let _ = term;
None
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()>;
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()>;
fn link_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> Result<()>;
fn unlink_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> Result<()>;
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn by_kid(&self, kid: &KeyID) -> Option<String>;
fn by_email(&self, email: &Email) -> Option<String>;
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>>;
/// Complex operation that updates a Cert in the database.
///
/// 1. Merge new Cert with old, full Cert
@@ -191,17 +199,18 @@ pub trait Database: Sync + Send {
/// - abort if any problems come up!
/// 5. Move full and published temporary Cert to their location
/// 6. Update all symlinks
fn merge(&self, new_tpk: Cert) -> Result<ImportResult> {
fn merge(&'a self, new_tpk: Cert) -> anyhow::Result<ImportResult> {
let fpr_primary = Fingerprint::try_from(new_tpk.primary_key().fingerprint())?;
let _lock = self.lock()?;
let tx = self.transaction()?;
let known_uids: Vec<UserID> = new_tpk
.userids()
.map(|binding| binding.userid().clone())
.collect();
let full_tpk_old = self.by_fpr_full(&fpr_primary)
let full_tpk_old = self
.by_fpr_full(&fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok());
let is_update = full_tpk_old.is_some();
let (full_tpk_new, full_tpk_unchanged) = if let Some(full_tpk_old) = full_tpk_old {
@@ -214,9 +223,14 @@ pub trait Database: Sync + Send {
let is_revoked = is_status_revoked(full_tpk_new.revocation_status(&POLICY, None));
let is_ok = is_revoked ||
full_tpk_new.keys().subkeys().next().is_some() ||
full_tpk_new.userids().next().is_some();
let is_ok = is_revoked
|| full_tpk_new.keys().subkeys().next().is_some()
|| full_tpk_new.userids().next().is_some()
|| full_tpk_new
.primary_key()
.self_signatures()
.next()
.is_some();
if !is_ok {
// self.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?;
return Err(anyhow!("Not a well-formed key!"));
@@ -225,7 +239,10 @@ pub trait Database: Sync + Send {
let published_tpk_old = self
.by_fpr(&fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok());
let published_emails = published_tpk_old.as_ref().map(|cert| tpk_get_emails(cert)).unwrap_or_default();
let published_emails = published_tpk_old
.as_ref()
.map(tpk_get_emails)
.unwrap_or_default();
let unparsed_uids = full_tpk_new
.userids()
@@ -235,15 +252,17 @@ pub trait Database: Sync + Send {
let mut email_status: Vec<_> = full_tpk_new
.userids()
.map(|binding| {
.filter_map(|binding| {
if let Ok(email) = Email::try_from(binding.userid()) {
Some((binding, email))
} else {
None
}
})
.flatten()
.filter(|(binding, email)| known_uids.contains(binding.userid()) || published_emails.contains(email))
.filter(|(binding, _)| binding.self_signatures().next().is_some())
.filter(|(binding, email)| {
known_uids.contains(binding.userid()) || published_emails.contains(email)
})
.flat_map(|(binding, email)| {
if is_status_revoked(binding.revocation_status(&POLICY, None)) {
Some((email, EmailAddressStatus::Revoked))
@@ -261,7 +280,11 @@ pub trait Database: Sync + Send {
// Abort if no changes were made
if full_tpk_unchanged {
return Ok(ImportResult::Unchanged(TpkStatus { is_revoked, email_status, unparsed_uids }));
return Ok(ImportResult::Unchanged(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}));
}
let published_tpk_new = if is_revoked {
@@ -277,37 +300,37 @@ pub trait Database: Sync + Send {
.userids()
.filter(|binding| !is_status_revoked(binding.revocation_status(&POLICY, None)))
.map(|binding| binding.userid())
.map(|uid| Email::try_from(uid).ok())
.flatten()
.flat_map(Email::try_from)
.any(|unrevoked_email| &unrevoked_email == *email);
!has_unrevoked_userid
}).collect();
})
.collect();
let fingerprints = tpk_get_linkable_fprs(&published_tpk_new);
let fpr_checks = fingerprints
.iter()
.map(|fpr| self.check_link_fpr(&fpr, &fpr_primary))
.map(|fpr| self.check_link_fpr(fpr, &fpr_primary))
.collect::<Vec<_>>()
.into_iter()
.collect::<Result<Vec<_>>>();
.collect::<anyhow::Result<Vec<_>>>();
if fpr_checks.is_err() {
self.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?;
tx.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?;
}
let fpr_checks = fpr_checks?;
let fpr_not_linked = fpr_checks.into_iter().flatten();
let full_tpk_tmp = self.write_to_temp(&tpk_to_string(&full_tpk_new)?)?;
let full_tpk_tmp = tx.write_to_temp(&tpk_to_string(&full_tpk_new)?)?;
let published_tpk_clean = tpk_clean(&published_tpk_new)?;
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
let published_tpk_tmp = tx.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
// these are very unlikely to fail. but if it happens,
// database consistency might be compromised!
self.move_tmp_to_full(full_tpk_tmp, &fpr_primary)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(&fpr_primary, &published_tpk_clean)?;
tx.move_tmp_to_full(full_tpk_tmp, &fpr_primary)?;
tx.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(&tx, &fpr_primary, &published_tpk_clean)?;
let published_tpk_changed = published_tpk_old
.map(|tpk| tpk != published_tpk_clean)
@@ -317,29 +340,39 @@ pub trait Database: Sync + Send {
}
for fpr in fpr_not_linked {
if let Err(e) = self.link_fpr(&fpr, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}",
&fpr, &fpr_primary, e);
if let Err(e) = tx.link_fpr(&fpr, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}", &fpr, &fpr_primary, e);
}
}
for revoked_email in newly_revoked_emails {
if let Err(e) = self.unlink_email(&revoked_email, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}",
&fpr_primary, &revoked_email, e);
if let Err(e) = tx.unlink_email(revoked_email, &fpr_primary) {
info!(
"Error ensuring symlink! {} {} {:?}",
&fpr_primary, &revoked_email, e
);
}
}
tx.commit()?;
if is_update {
Ok(ImportResult::Updated(TpkStatus { is_revoked, email_status, unparsed_uids }))
Ok(ImportResult::Updated(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}))
} else {
Ok(ImportResult::New(TpkStatus { is_revoked, email_status, unparsed_uids }))
Ok(ImportResult::New(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}))
}
}
fn update_write_log(&self, fpr_primary: &Fingerprint) {
let log_name = self.get_current_log_filename();
println!("{}", log_name);
if let Err(e) = self.write_log_append(&log_name, fpr_primary) {
error!("Error writing to log! {} {} {}", &log_name, &fpr_primary, e);
}
@@ -349,8 +382,13 @@ pub trait Database: Sync + Send {
Utc::now().format("%Y-%m-%d").to_string()
}
fn get_tpk_status(&self, fpr_primary: &Fingerprint, known_addresses: &[Email]) -> Result<TpkStatus> {
let tpk_full = self.by_fpr_full(&fpr_primary)
fn get_tpk_status(
&self,
fpr_primary: &Fingerprint,
known_addresses: &[Email],
) -> anyhow::Result<TpkStatus> {
let tpk_full = self
.by_fpr_full(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
@@ -363,15 +401,18 @@ pub trait Database: Sync + Send {
.count();
let published_uids: Vec<UserID> = self
.by_fpr(&fpr_primary)
.by_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.map(|tpk| tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
).unwrap_or_default();
.map(|tpk| {
tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
})
.unwrap_or_default();
let mut email_status: Vec<_> = tpk_full
.userids()
.filter(|binding| binding.self_signatures().next().is_some())
.flat_map(|binding| {
let uid = binding.userid();
if let Ok(email) = Email::try_from(uid) {
@@ -394,7 +435,11 @@ pub trait Database: Sync + Send {
// the same address, we keep the first.
email_status.dedup_by(|(e1, _), (e2, _)| e1 == e2);
Ok(TpkStatus { is_revoked, email_status, unparsed_uids })
Ok(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
})
}
/// Complex operation that publishes some user id for a Cert already in the database.
@@ -410,74 +455,88 @@ pub trait Database: Sync + Send {
/// - abort if any problems come up!
/// 5. Move full and published temporary Cert to their location
/// 6. Update all symlinks
fn set_email_published(&self, fpr_primary: &Fingerprint, email_new: &Email) -> Result<()> {
let _lock = self.lock()?;
fn set_email_published(
&'a self,
fpr_primary: &Fingerprint,
email_new: &Email,
) -> anyhow::Result<()> {
let tx = self.transaction()?;
self.nolock_unlink_email_if_other(fpr_primary, email_new)?;
self.unlink_email_if_other(&tx, fpr_primary, email_new)?;
let full_tpk = self.by_fpr_full(&fpr_primary)
let full_tpk = self
.by_fpr_full(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
let published_uids_old: Vec<UserID> = self
.by_fpr(&fpr_primary)
.by_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.map(|tpk| tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
).unwrap_or_default();
let published_emails_old: Vec<Email> = published_uids_old.iter()
.map(|uid| Email::try_from(uid).ok())
.flatten()
.map(|tpk| {
tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
})
.unwrap_or_default();
let published_emails_old: Vec<Email> = published_uids_old
.iter()
.flat_map(Email::try_from)
.collect();
// println!("publishing: {:?}", &uid_new);
if published_emails_old.contains(&email_new) {
if published_emails_old.contains(email_new) {
// UserID already published - just stop
return Ok(());
}
let mut published_emails = published_emails_old.clone();
let mut published_emails = published_emails_old;
published_emails.push(email_new.clone());
let published_tpk_new = tpk_filter_alive_emails(&full_tpk, &published_emails);
if !published_tpk_new
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.any(|email| email == *email_new) {
return Err(anyhow!("Requested UserID not found!"));
.flat_map(|binding| Email::try_from(binding.userid()))
.any(|email| email == *email_new)
{
return Err(anyhow!("Requested UserID not found!"));
}
let published_tpk_clean = tpk_clean(&published_tpk_new)?;
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
let published_tpk_tmp = tx.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(fpr_primary, &published_tpk_clean)?;
tx.move_tmp_to_published(published_tpk_tmp, fpr_primary)?;
self.regenerate_wkd(&tx, fpr_primary, &published_tpk_clean)?;
self.update_write_log(&fpr_primary);
self.update_write_log(fpr_primary);
if let Err(e) = self.link_email(&email_new, &fpr_primary) {
info!("Error ensuring email symlink! {} -> {} {:?}",
&email_new, &fpr_primary, e);
if let Err(e) = tx.link_email(email_new, fpr_primary) {
info!(
"Error ensuring email symlink! {} -> {} {:?}",
&email_new, &fpr_primary, e
);
}
tx.commit()?;
Ok(())
}
fn nolock_unlink_email_if_other(
fn unlink_email_if_other(
&self,
tx: &Self::Transaction,
fpr_primary: &Fingerprint,
unlink_email: &Email,
) -> Result<()> {
let current_link_fpr = self.lookup_primary_fingerprint(
&Query::ByEmail(unlink_email.clone()));
) -> anyhow::Result<()> {
let current_link_fpr =
self.lookup_primary_fingerprint(&Query::ByEmail(unlink_email.clone()));
if let Some(current_fpr) = current_link_fpr {
if current_fpr != *fpr_primary {
self.nolock_set_email_unpublished_filter(&current_fpr,
|uid| Email::try_from(uid).map(|email| email != *unlink_email)
.unwrap_or(false))?;
self.set_email_unpublished_filter(tx, &current_fpr, |uid| {
Email::try_from(uid)
.map(|email| email != *unlink_email)
.unwrap_or(false)
})?;
}
}
Ok(())
@@ -498,35 +557,25 @@ pub trait Database: Sync + Send {
/// 6. Update all symlinks
fn set_email_unpublished_filter(
&self,
tx: &Self::Transaction,
fpr_primary: &Fingerprint,
email_remove: impl Fn(&UserID) -> bool,
) -> Result<()> {
let _lock = self.lock()?;
self.nolock_set_email_unpublished_filter(fpr_primary, email_remove)
}
fn nolock_set_email_unpublished_filter(
&self,
fpr_primary: &Fingerprint,
email_remove: impl Fn(&UserID) -> bool,
) -> Result<()> {
let published_tpk_old = self.by_fpr(&fpr_primary)
) -> anyhow::Result<()> {
let published_tpk_old = self
.by_fpr(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
let published_emails_old: Vec<Email> = published_tpk_old
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect();
let published_tpk_new = published_tpk_old.clone().retain_userids(
|uid| email_remove(uid.userid()));
let published_tpk_new = published_tpk_old.retain_userids(|uid| email_remove(uid.userid()));
let published_emails_new: Vec<Email> = published_tpk_new
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect();
let unpublished_emails = published_emails_old
@@ -534,17 +583,19 @@ pub trait Database: Sync + Send {
.filter(|email| !published_emails_new.contains(email));
let published_tpk_clean = tpk_clean(&published_tpk_new)?;
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
let published_tpk_tmp = tx.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(fpr_primary, &published_tpk_clean)?;
tx.move_tmp_to_published(published_tpk_tmp, fpr_primary)?;
self.regenerate_wkd(tx, fpr_primary, &published_tpk_clean)?;
self.update_write_log(&fpr_primary);
self.update_write_log(fpr_primary);
for unpublished_email in unpublished_emails {
if let Err(e) = self.unlink_email(&unpublished_email, &fpr_primary) {
info!("Error deleting email symlink! {} -> {} {:?}",
&unpublished_email, &fpr_primary, e);
if let Err(e) = tx.unlink_email(unpublished_email, fpr_primary) {
info!(
"Error deleting email symlink! {} -> {} {:?}",
&unpublished_email, &fpr_primary, e
);
}
}
@@ -552,47 +603,50 @@ pub trait Database: Sync + Send {
}
fn set_email_unpublished(
&self,
&'a self,
fpr_primary: &Fingerprint,
email_remove: &Email,
) -> Result<()> {
self.set_email_unpublished_filter(fpr_primary, |uid|
) -> anyhow::Result<()> {
let tx = self.transaction().unwrap();
self.set_email_unpublished_filter(&tx, fpr_primary, |uid| {
Email::try_from(uid)
.map(|email| email != *email_remove)
.unwrap_or(false))
.unwrap_or(false)
})?;
tx.commit()?;
Ok(())
}
fn set_email_unpublished_all(
&self,
fpr_primary: &Fingerprint,
) -> Result<()> {
self.set_email_unpublished_filter(fpr_primary, |_| false)
fn set_email_unpublished_all(&'a self, fpr_primary: &Fingerprint) -> anyhow::Result<()> {
let tx = self.transaction().unwrap();
self.set_email_unpublished_filter(&tx, fpr_primary, |_| false)?;
tx.commit()?;
Ok(())
}
fn regenerate_links(
&self,
fpr_primary: &Fingerprint,
) -> Result<RegenerateResult> {
let tpk = self.by_primary_fpr(&fpr_primary)
fn regenerate_links(&'a self, fpr_primary: &Fingerprint) -> anyhow::Result<RegenerateResult> {
let tx = self.transaction().unwrap();
let tpk = self
.by_primary_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.ok_or_else(|| anyhow!("Key not in database!"))?;
let published_emails: Vec<Email> = tpk
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect();
self.regenerate_wkd(fpr_primary, &tpk)?;
self.regenerate_wkd(&tx, fpr_primary, &tpk)?;
let fingerprints = tpk_get_linkable_fprs(&tpk);
let fpr_checks = fingerprints
.into_iter()
.map(|fpr| self.check_link_fpr(&fpr, &fpr_primary))
.map(|fpr| self.check_link_fpr(&fpr, fpr_primary))
.collect::<Vec<_>>()
.into_iter()
.collect::<Result<Vec<_>>>()?;
.collect::<anyhow::Result<Vec<_>>>()?;
let fpr_not_linked = fpr_checks.into_iter().flatten();
@@ -601,14 +655,16 @@ pub trait Database: Sync + Send {
for fpr in fpr_not_linked {
keys_linked += 1;
self.link_fpr(&fpr, &fpr_primary)?;
tx.link_fpr(&fpr, fpr_primary)?;
}
for email in published_emails {
emails_linked += 1;
self.link_email(&email, &fpr_primary)?;
tx.link_email(&email, fpr_primary)?;
}
tx.commit()?;
if keys_linked != 0 || emails_linked != 0 {
Ok(RegenerateResult::Updated)
} else {
@@ -618,59 +674,47 @@ pub trait Database: Sync + Send {
fn regenerate_wkd(
&self,
tx: &Self::Transaction,
fpr_primary: &Fingerprint,
published_tpk: &Cert
) -> Result<()> {
published_tpk: &Cert,
) -> anyhow::Result<()> {
let published_wkd_tpk_tmp = if published_tpk.userids().next().is_some() {
Some(self.write_to_temp(&published_tpk.to_vec()?)?)
Some(tx.write_to_temp(&published_tpk.export_to_vec()?)?)
} else {
None
};
self.move_tmp_to_published_wkd(published_wkd_tpk_tmp, fpr_primary)?;
tx.move_tmp_to_published_wkd(published_wkd_tpk_tmp, fpr_primary)?;
Ok(())
}
fn check_link_fpr(&self, fpr: &Fingerprint, target: &Fingerprint) -> Result<Option<Fingerprint>>;
fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String>;
fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn write_to_temp(&self, content: &[u8]) -> Result<NamedTempFile>;
fn move_tmp_to_full(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>;
fn move_tmp_to_published(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>;
fn move_tmp_to_published_wkd(&self, content: Option<NamedTempFile>, fpr: &Fingerprint) -> Result<()>;
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()>;
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()>;
fn check_consistency(&self) -> Result<()>;
}
fn tpk_get_emails(cert: &Cert) -> Vec<Email> {
cert
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
cert.userids()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect()
}
pub fn tpk_get_linkable_fprs(tpk: &Cert) -> Vec<Fingerprint> {
let ref signing_capable = KeyFlags::empty()
.set_signing()
.set_certification();
let ref fpr_primary = Fingerprint::try_from(tpk.fingerprint()).unwrap();
tpk
.keys()
.into_iter()
.flat_map(|bundle| {
Fingerprint::try_from(bundle.key().fingerprint())
.map(|fpr| (fpr, bundle.binding_signature(&POLICY, None).ok().and_then(|sig| sig.key_flags())))
let signing_capable = &KeyFlags::empty().set_signing().set_certification();
let fpr_primary = &Fingerprint::try_from(tpk.fingerprint()).unwrap();
tpk.keys()
.flat_map(|bundle| {
Fingerprint::try_from(bundle.key().fingerprint()).map(|fpr| {
(
fpr,
bundle
.binding_signature(&POLICY, None)
.ok()
.and_then(|sig| sig.key_flags()),
)
})
.filter(|(fpr, flags)| {
fpr == fpr_primary ||
flags.is_none() ||
!(signing_capable & flags.as_ref().unwrap()).is_empty()
})
.map(|(fpr,_)| fpr)
.collect()
})
.filter(|(fpr, flags)| {
fpr == fpr_primary
|| flags.is_none()
|| !(signing_capable & flags.as_ref().unwrap()).is_empty()
})
.map(|(fpr, _)| fpr)
.collect()
}

View File

@@ -1,15 +1,11 @@
use openpgp::Result;
use std::convert::TryFrom;
use openpgp::{
Cert,
use sequoia_openpgp::{
Cert, Result, cert::prelude::*, policy::StandardPolicy, serialize::SerializeInto as _,
types::RevocationStatus,
cert::prelude::*,
serialize::SerializeInto as _,
policy::StandardPolicy,
};
use Email;
use crate::Email;
pub const POLICY: StandardPolicy = StandardPolicy::new();
@@ -33,24 +29,42 @@ pub fn tpk_clean(tpk: &Cert) -> Result<Cert> {
// The primary key and related signatures.
let pk_bundle = tpk.primary_key().bundle();
acc.push(pk_bundle.key().clone().into());
for s in pk_bundle.self_signatures() { acc.push(s.clone().into()) }
for s in pk_bundle.self_revocations() { acc.push(s.clone().into()) }
for s in pk_bundle.other_revocations() { acc.push(s.clone().into()) }
for s in pk_bundle.self_signatures() {
acc.push(s.clone().into())
}
for s in pk_bundle.self_revocations() {
acc.push(s.clone().into())
}
for s in pk_bundle.other_revocations() {
acc.push(s.clone().into())
}
// The subkeys and related signatures.
for skb in tpk.keys().subkeys() {
acc.push(skb.key().clone().into());
for s in skb.self_signatures() { acc.push(s.clone().into()) }
for s in skb.self_revocations() { acc.push(s.clone().into()) }
for s in skb.other_revocations() { acc.push(s.clone().into()) }
for s in skb.self_signatures() {
acc.push(s.clone().into())
}
for s in skb.self_revocations() {
acc.push(s.clone().into())
}
for s in skb.other_revocations() {
acc.push(s.clone().into())
}
}
// The UserIDs.
for uidb in tpk.userids() {
acc.push(uidb.userid().clone().into());
for s in uidb.self_signatures() { acc.push(s.clone().into()) }
for s in uidb.self_revocations() { acc.push(s.clone().into()) }
for s in uidb.other_revocations() { acc.push(s.clone().into()) }
for s in uidb.self_signatures() {
acc.push(s.clone().into())
}
for s in uidb.self_revocations() {
acc.push(s.clone().into())
}
for s in uidb.other_revocations() {
acc.push(s.clone().into())
}
// Reasoning about the currently attested certifications
// requires a policy.
@@ -70,7 +84,8 @@ pub fn tpk_clean(tpk: &Cert) -> Result<Cert> {
/// Filters the Cert, keeping only UserIDs that aren't revoked, and whose emails match the given list
pub fn tpk_filter_alive_emails(tpk: &Cert, emails: &[Email]) -> Cert {
tpk.clone().retain_userids(|uid| {
if is_status_revoked(uid.revocation_status(&POLICY, None)) {
let is_exportable = uid.self_signatures().any(|s| s.exportable().is_ok());
if !is_exportable || is_status_revoked(uid.revocation_status(&POLICY, None)) {
false
} else if let Ok(email) = Email::try_from(uid.userid()) {
emails.contains(&email)

782
database/src/sqlite.rs Normal file
View File

@@ -0,0 +1,782 @@
use self_cell::self_cell;
use std::convert::TryFrom;
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::types::{Email, Fingerprint, KeyID};
use crate::{Database, Query};
use anyhow::{anyhow, format_err};
use sequoia_openpgp::{Cert, policy::StandardPolicy};
use std::time::{SystemTime, UNIX_EPOCH};
use r2d2_sqlite::SqliteConnectionManager;
use r2d2_sqlite::rusqlite::OptionalExtension;
use r2d2_sqlite::rusqlite::ToSql;
use r2d2_sqlite::rusqlite::Transaction;
use r2d2_sqlite::rusqlite::params;
use crate::{DatabaseTransaction, wkd};
pub const POLICY: StandardPolicy = StandardPolicy::new();
const DEFAULT_DB_FILE_NAME: &str = "keys.sqlite";
const DEFAULT_LOG_DIR_NAME: &str = "log";
pub struct Sqlite {
pool: r2d2::Pool<SqliteConnectionManager>,
}
impl Sqlite {
pub fn new_file(db_file: impl AsRef<Path>, log_dir: impl AsRef<Path>) -> anyhow::Result<Self> {
create_dir_all(log_dir)?;
Self::new_internal(SqliteConnectionManager::file(db_file))
}
pub fn log_dir_path(base_dir: impl AsRef<Path>) -> PathBuf {
base_dir.as_ref().join(DEFAULT_LOG_DIR_NAME)
}
pub fn log_dir_path_from_db_file_path(
db_file_path: impl AsRef<Path>,
) -> anyhow::Result<PathBuf> {
db_file_path
.as_ref()
.parent()
.ok_or_else(|| {
anyhow!(
"Can't get log dir path from invalid db file path: {:?}",
db_file_path.as_ref()
)
})
.map(|parent_dir_path| parent_dir_path.join(DEFAULT_LOG_DIR_NAME))
}
pub fn db_file_path(base_dir: impl AsRef<Path>) -> PathBuf {
base_dir.as_ref().join(DEFAULT_DB_FILE_NAME)
}
#[cfg(test)]
fn build_pool(
manager: SqliteConnectionManager,
) -> anyhow::Result<r2d2::Pool<SqliteConnectionManager>> {
#[derive(Copy, Clone, Debug)]
pub struct LogConnectionCustomizer;
impl<E> r2d2::CustomizeConnection<rusqlite::Connection, E> for LogConnectionCustomizer {
fn on_acquire(&self, conn: &mut rusqlite::Connection) -> Result<(), E> {
println!("Acquiring sqlite pool connection: {:?}", conn);
conn.trace(Some(|query| {
println!("{}", query);
}));
Ok(())
}
fn on_release(&self, conn: rusqlite::Connection) {
println!("Releasing pool connection: {:?}", conn);
}
}
Ok(r2d2::Pool::builder()
.connection_customizer(Box::new(LogConnectionCustomizer {}))
.build(manager)?)
}
#[cfg(not(test))]
fn build_pool(
manager: SqliteConnectionManager,
) -> anyhow::Result<r2d2::Pool<SqliteConnectionManager>> {
Ok(r2d2::Pool::builder().build(manager)?)
}
fn new_internal(manager: SqliteConnectionManager) -> anyhow::Result<Self> {
let pool = Self::build_pool(manager)?;
let conn = pool.get()?;
conn.pragma_update(None, "journal_mode", "wal")?;
conn.pragma_update(None, "synchronous", "normal")?;
conn.pragma_update(None, "user_version", "1")?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS certs (
primary_fingerprint TEXT NOT NULL PRIMARY KEY,
full TEXT NOT NULL,
published TEXT,
published_not_armored BLOB,
updated_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS cert_identifiers (
fingerprint TEXT NOT NULL PRIMARY KEY,
keyid TEXT NOT NULL,
primary_fingerprint TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS emails (
email TEXT NOT NULL PRIMARY KEY,
domain TEXT NOT NULL,
wkd_hash TEXT NOT NULL,
primary_fingerprint TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);
",
)?;
Ok(Self { pool })
}
}
self_cell! {
pub struct SqliteTransaction {
owner: r2d2::PooledConnection<SqliteConnectionManager>,
#[covariant]
dependent: Transaction,
}
}
impl SqliteTransaction {
fn start(pool: &r2d2::Pool<SqliteConnectionManager>) -> anyhow::Result<Self> {
let conn = pool.get()?;
Ok(Self::new(conn, |c| {
Transaction::new_unchecked(c, rusqlite::TransactionBehavior::Deferred).unwrap()
}))
}
fn tx(&self) -> &Transaction<'_> {
self.borrow_dependent()
}
}
fn query_simple<T: rusqlite::types::FromSql>(
conn: &r2d2::PooledConnection<SqliteConnectionManager>,
query: &str,
params: &[&dyn ToSql],
) -> Option<T> {
conn.prepare_cached(query)
.expect("query must be valid")
.query_row(params, |row| row.get(0))
.optional()
.expect("query exection must not fail")
}
impl DatabaseTransaction<'_> for SqliteTransaction {
type TempCert = Vec<u8>;
fn commit(self) -> anyhow::Result<()> {
// we can't use tx().commit(), but we can cheat :)
self.tx().execute_batch("COMMIT")?;
Ok(())
}
fn write_to_temp(&self, content: &[u8]) -> anyhow::Result<Self::TempCert> {
Ok(content.to_vec())
}
fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
let file = String::from_utf8(file)?;
self.tx().execute(
"
INSERT INTO certs (primary_fingerprint, full, created_at, updated_at)
VALUES (?1, ?2, ?3, ?3)
ON CONFLICT(primary_fingerprint) DO UPDATE SET full=excluded.full, updated_at = excluded.updated_at
",
params![fpr, file, now],
)?;
Ok(())
}
fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
let file = String::from_utf8(file)?;
self.tx().execute(
"UPDATE certs SET published = ?2, updated_at = ?3 WHERE primary_fingerprint = ?1",
params![fpr, file, now],
)?;
Ok(())
}
fn move_tmp_to_published_wkd(
&self,
file: Option<Self::TempCert>,
fpr: &Fingerprint,
) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
self.tx().execute(
"UPDATE certs SET published_not_armored = ?2, updated_at = ?3 WHERE primary_fingerprint = ?1",
params![fpr, file, now],
)?;
Ok(())
}
fn write_to_quarantine(&self, _fpr: &Fingerprint, _content: &[u8]) -> anyhow::Result<()> {
Ok(())
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
let (domain, wkd_hash) = wkd::encode_wkd(email.as_str()).expect("email must be vaild");
self.tx().execute(
"
INSERT INTO emails (email, wkd_hash, domain, primary_fingerprint, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(email) DO UPDATE SET primary_fingerprint = excluded.primary_fingerprint
",
params![email, domain, wkd_hash, fpr, now],
)?;
Ok(())
}
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()> {
self.tx()
.execute(
"DELETE FROM emails WHERE email = ?1 AND primary_fingerprint = ?2",
params![email, fpr],
)
.unwrap();
Ok(())
}
fn link_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
self.tx().execute(
"
INSERT INTO cert_identifiers (fingerprint, keyid, primary_fingerprint, created_at)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(fingerprint) DO UPDATE SET primary_fingerprint = excluded.primary_fingerprint;
",
params![
from_fpr,
KeyID::from(from_fpr),
primary_fpr,
now,
],
)?;
Ok(())
}
fn unlink_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> anyhow::Result<()> {
self.tx().execute(
"DELETE FROM cert_identifiers WHERE primary_fingerprint = ?1 AND fingerprint = ?2 AND keyid = ?3",
params![primary_fpr, from_fpr, KeyID::from(from_fpr)],
)?;
Ok(())
}
}
impl<'a> Database<'a> for Sqlite {
type Transaction = SqliteTransaction;
fn transaction(&'a self) -> anyhow::Result<Self::Transaction> {
SqliteTransaction::start(&self.pool)
}
fn write_log_append(&self, _filename: &str, _fpr_primary: &Fingerprint) -> anyhow::Result<()> {
// this is done implicitly via created_at in sqlite, no need to do anything here
Ok(())
}
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint> {
use super::Query::*;
let conn = self.pool.get().unwrap();
match term {
ByFingerprint(fp) => query_simple(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE fingerprint = ?1",
params![fp],
),
ByKeyID(keyid) => query_simple(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE keyid = ?1",
params![keyid],
),
ByEmail(email) => query_simple(
&conn,
"SELECT primary_fingerprint FROM emails WHERE email = ?1",
params![email],
),
_ => None,
}
}
// Lookup straight from certs table, no link resolution
fn by_fpr_full(&self, primary_fpr: &Fingerprint) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple(
&conn,
"SELECT full FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
}
// XXX: rename! to by_primary_fpr_published
// Lookup the published cert straight from certs table, no link resolution
fn by_primary_fpr(&self, primary_fpr: &Fingerprint) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
}
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE fingerprint = ?1",
params![fpr],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![&primary_fpr],
)
})
}
fn by_email(&self, email: &Email) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM emails WHERE email = ?1",
params![email],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![&primary_fpr],
)
})
}
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM emails WHERE email = ?1",
params![email],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published_not_armored FROM certs WHERE primary_fingerprint = ?1",
params![&primary_fpr],
)
})
}
fn by_kid(&self, kid: &KeyID) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE keyid = ?1",
params![kid],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
})
}
fn by_domain_and_hash_wkd(&self, domain: &str, wkd_hash: &str) -> Option<Vec<u8>> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM emails WHERE domain = ?1 AND wkd_hash = ?2",
params![domain, wkd_hash],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published_not_armored FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
})
}
fn check_link_fpr(
&self,
fpr: &Fingerprint,
_fpr_target: &Fingerprint,
) -> anyhow::Result<Option<Fingerprint>> {
// a desync here cannot happen structurally, so always return true here
Ok(Some(fpr.clone()))
}
/// Checks the database for consistency.
///
/// Note that this operation may take a long time, and is
/// generally only useful for testing.
fn check_consistency(&self) -> anyhow::Result<()> {
let conn = self.pool.get().unwrap();
let mut stmt = conn.prepare("SELECT primary_fingerprint, published FROM certs")?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let primary_fpr: Fingerprint = row.get(0)?;
let published: String = row.get(1)?;
let cert = Cert::from_str(&published).unwrap();
let mut cert_emails: Vec<Email> = cert
.userids()
.filter_map(|uid| uid.userid().email2().unwrap())
.flat_map(Email::from_str)
.collect();
let mut db_emails: Vec<Email> = conn
.prepare("SELECT email FROM emails WHERE primary_fingerprint = ?1")?
.query_map([&primary_fpr], |row| row.get::<_, String>(0))
.unwrap()
.flat_map(|email| Email::from_str(&email.unwrap()))
.collect();
cert_emails.sort();
cert_emails.dedup();
db_emails.sort();
if cert_emails != db_emails {
return Err(format_err!(
"{:?} does not have correct emails indexed, cert ${:?} db {:?}",
&primary_fpr,
cert_emails,
db_emails,
));
}
let policy = &POLICY;
let mut cert_fprs: Vec<Fingerprint> = cert
.keys()
.with_policy(policy, None)
.for_certification()
.for_signing()
.map(|amalgamation| amalgamation.key().fingerprint())
.flat_map(Fingerprint::try_from)
.collect();
let mut db_fprs: Vec<Fingerprint> = conn
.prepare("SELECT fingerprint FROM cert_identifiers WHERE primary_fingerprint = ?1")?
.query_map([&primary_fpr], |row| row.get::<_, Fingerprint>(0))
.unwrap()
.flatten()
.collect();
cert_fprs.sort();
db_fprs.sort();
if cert_fprs != db_fprs {
return Err(format_err!(
"{:?} does not have correct fingerprints indexed, cert ${:?} db {:?}",
&primary_fpr,
cert_fprs,
db_fprs,
));
}
}
Ok(())
}
fn get_last_log_entry(&self) -> anyhow::Result<Fingerprint> {
let conn = self.pool.get().unwrap();
Ok(conn.query_row(
"SELECT primary_fingerprint FROM certs ORDER BY updated_at DESC LIMIT 1",
[],
|row| row.get::<_, Fingerprint>(0),
)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test;
use sequoia_openpgp::cert::CertBuilder;
use tempfile::TempDir;
const DATA_1: &str = "data, content doesn't matter";
const DATA_2: &str = "other data, content doesn't matter";
const FINGERPRINT_1: &str = "D4AB192964F76A7F8F8A9B357BD18320DEADFA11";
fn open_db() -> (TempDir, Sqlite) {
let tmpdir = TempDir::new().unwrap();
let tempdir_path = tmpdir.path();
let db = Sqlite::new_file(
Sqlite::db_file_path(tempdir_path),
Sqlite::log_dir_path(tempdir_path),
)
.unwrap();
(tmpdir, db)
}
#[test]
fn new() {
let (_tmp_dir, db) = open_db();
let k1 = CertBuilder::new()
.add_userid("a@invalid.example.org")
.generate()
.unwrap()
.0;
let k2 = CertBuilder::new()
.add_userid("b@invalid.example.org")
.generate()
.unwrap()
.0;
let k3 = CertBuilder::new()
.add_userid("c@invalid.example.org")
.generate()
.unwrap()
.0;
assert!(
!db.merge(k1)
.unwrap()
.into_tpk_status()
.email_status
.is_empty()
);
assert!(
!db.merge(k2.clone())
.unwrap()
.into_tpk_status()
.email_status
.is_empty()
);
assert!(!db.merge(k2).unwrap().into_tpk_status().email_status.len() > 0);
assert!(
!db.merge(k3.clone())
.unwrap()
.into_tpk_status()
.email_status
.is_empty()
);
assert!(
!db.merge(k3.clone())
.unwrap()
.into_tpk_status()
.email_status
.len()
> 0
);
assert!(!db.merge(k3).unwrap().into_tpk_status().email_status.len() > 0);
}
#[test]
fn xx_by_fpr_full() -> anyhow::Result<()> {
let (_tmp_dir, db) = open_db();
let fpr1 = Fingerprint::from_str(FINGERPRINT_1)?;
let lock = db.transaction().unwrap();
lock.move_tmp_to_full(lock.write_to_temp(DATA_1.as_bytes())?, &fpr1)?;
lock.link_fpr(&fpr1, &fpr1)?;
lock.commit().unwrap();
assert_eq!(db.by_fpr_full(&fpr1).expect("must find key"), DATA_1);
Ok(())
}
#[test]
fn xx_by_kid() -> anyhow::Result<()> {
let (_tmp_dir, db) = open_db();
let fpr1 = Fingerprint::from_str(FINGERPRINT_1)?;
let lock = db.transaction().unwrap();
lock.move_tmp_to_full(lock.write_to_temp(DATA_1.as_bytes())?, &fpr1)?;
lock.move_tmp_to_published(lock.write_to_temp(DATA_2.as_bytes())?, &fpr1)?;
lock.link_fpr(&fpr1, &fpr1)?;
lock.commit().unwrap();
assert_eq!(db.by_kid(&fpr1.into()).expect("must find key"), DATA_2);
Ok(())
}
#[test]
fn xx_by_primary_fpr() -> anyhow::Result<()> {
let (_tmp_dir, db) = open_db();
let fpr1 = Fingerprint::from_str(FINGERPRINT_1)?;
let lock = db.transaction().unwrap();
lock.move_tmp_to_full(lock.write_to_temp(DATA_1.as_bytes())?, &fpr1)?;
lock.move_tmp_to_published(lock.write_to_temp(DATA_2.as_bytes())?, &fpr1)?;
lock.commit().unwrap();
assert_eq!(db.by_primary_fpr(&fpr1).expect("must find key"), DATA_2);
Ok(())
}
#[test]
fn uid_verification() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_verification(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_deletion() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_deletion(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn subkey_lookup() {
let (_tmp_dir, mut db) = open_db();
test::test_subkey_lookup(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn kid_lookup() {
let (_tmp_dir, mut db) = open_db();
test::test_kid_lookup(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn upload_revoked_tpk() {
let (_tmp_dir, mut db) = open_db();
test::test_upload_revoked_tpk(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_revocation() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_revocation(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn regenerate() {
let (_tmp_dir, mut db) = open_db();
test::test_regenerate(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn key_reupload() {
let (_tmp_dir, mut db) = open_db();
test::test_reupload(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_replacement() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_replacement(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_unlinking() {
let (_tmp_dir, mut db) = open_db();
test::test_unlink_uid(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_1() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_1(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_2() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_2(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_3() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_3(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_4() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_4(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn no_selfsig() {
let (_tmp_dir, mut db) = open_db();
test::test_no_selfsig(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn allow_dks() {
let (_tmp_dir, mut db) = open_db();
test::test_allow_dks(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn allow_revoked() {
let (_tmp_dir, mut db) = open_db();
test::test_allow_revoked(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn bad_uids() {
let (_tmp_dir, mut db) = open_db();
test::test_bad_uids(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn reverse_fingerprint_to_path() {
let tmpdir = TempDir::new().unwrap();
let tmpdir_path = tmpdir.path();
let db = Sqlite::new_file(
Sqlite::db_file_path(tmpdir_path),
Sqlite::log_dir_path(tmpdir_path),
)
.unwrap();
let _fp: Fingerprint = "CBCD8F030588653EEDD7E2659B7DD433F254904A".parse().unwrap();
// XXX: fixme
//assert_eq!(Sqlite::path_to_fingerprint(&db.link_by_fingerprint(&fp)),
// Some(fp.clone()));
db.check_consistency().expect("inconsistent database");
}
#[test]
fn attested_key_signatures() -> anyhow::Result<()> {
let (_tmp_dir, mut db) = open_db();
test::attested_key_signatures(&mut db)?;
db.check_consistency()?;
Ok(())
}
#[test]
fn nonexportable_sigs() -> anyhow::Result<()> {
let (_tmp_dir, mut db) = open_db();
test::nonexportable_sigs(&mut db)?;
db.check_consistency()?;
Ok(())
}
}

View File

@@ -1,17 +1,16 @@
use std::io::{Read,Write};
use std::fs::{File, create_dir_all, remove_file};
use std::io::{Read, Write};
use std::path::PathBuf;
use std::fs::{create_dir_all, remove_file, File};
use log::info;
use std::str;
use Result;
pub struct StatefulTokens {
token_dir: PathBuf,
}
impl StatefulTokens {
pub fn new(token_dir: impl Into<PathBuf>) -> Result<Self> {
pub fn new(token_dir: impl Into<PathBuf>) -> anyhow::Result<Self> {
let token_dir = token_dir.into();
create_dir_all(&token_dir)?;
@@ -21,9 +20,9 @@ impl StatefulTokens {
Ok(StatefulTokens { token_dir })
}
pub fn new_token(&self, token_type: &str, payload: &[u8]) -> Result<String> {
pub fn new_token(&self, token_type: &str, payload: &[u8]) -> anyhow::Result<String> {
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rand::{Rng, thread_rng};
let mut rng = thread_rng();
// samples from [a-zA-Z0-9]
@@ -38,7 +37,7 @@ impl StatefulTokens {
Ok(name)
}
pub fn pop_token(&self, token_type: &str, token: &str) -> Result<String> {
pub fn pop_token(&self, token_type: &str, token: &str) -> anyhow::Result<String> {
let path = self.token_dir.join(token_type).join(token);
let buf = {
let mut fd = File::open(&path)?;

View File

@@ -4,8 +4,6 @@ use std::path::Path;
use fs2::FileExt;
use Result;
/// A minimalistic flock-based mutex.
///
/// This just barely implements enough what we need from a mutex.
@@ -14,7 +12,7 @@ pub struct FlockMutexGuard {
}
impl FlockMutexGuard {
pub fn lock(path: impl AsRef<Path>) -> Result<Self> {
pub fn lock(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let file = File::open(path)?;
while let Err(e) = file.lock_exclusive() {
// According to flock(2), possible errors returned are:

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,16 @@
use std::convert::TryFrom;
use std::fmt;
use std::result;
use std::str::FromStr;
use openpgp::packet::UserID;
use anyhow::anyhow;
use hex::ToHex;
use r2d2_sqlite::rusqlite::types::FromSql;
use r2d2_sqlite::rusqlite::types::FromSqlError;
use r2d2_sqlite::rusqlite::types::FromSqlResult;
use r2d2_sqlite::rusqlite::types::ToSql;
use r2d2_sqlite::rusqlite::types::ValueRef;
use sequoia_openpgp::packet::UserID;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use anyhow::Error;
use {Result};
/// Holds a normalized email address.
///
@@ -26,11 +30,27 @@ impl Email {
}
}
impl TryFrom<&UserID> for Email {
type Error = Error;
impl FromSql for Email {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
value
.as_str()
.and_then(|s| Self::from_str(s).map_err(|_| FromSqlError::InvalidType))
}
}
fn try_from(uid: &UserID) -> Result<Self> {
if let Some(address) = uid.email()? {
impl ToSql for Email {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Borrowed(
rusqlite::types::ValueRef::Text(self.0.as_bytes()),
))
}
}
impl TryFrom<&UserID> for Email {
type Error = anyhow::Error;
fn try_from(uid: &UserID) -> anyhow::Result<Self> {
if let Some(address) = uid.email2()? {
let mut iter = address.split('@');
let localpart = iter.next().expect("Invalid email address");
let domain = iter.next().expect("Invalid email address");
@@ -41,7 +61,7 @@ impl TryFrom<&UserID> for Email {
.map_err(|e| anyhow!("punycode conversion failed: {:?}", e))?;
// TODO this is a hotfix for a lettre vulnerability. remove once fixed upstream.
if localpart.starts_with("-") {
if localpart.starts_with('-') {
return Err(anyhow!("malformed email address: '{:?}'", uid.value()));
}
@@ -70,24 +90,39 @@ impl fmt::Display for Email {
}
impl FromStr for Email {
type Err = Error;
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Email> {
fn from_str(s: &str) -> anyhow::Result<Email> {
Email::try_from(&UserID::from(s))
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Fingerprint([u8; 20]);
impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
type Error = Error;
impl FromSql for Fingerprint {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
value
.as_str()
.and_then(|s| Self::from_str(s).map_err(|_| FromSqlError::InvalidType))
}
}
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
impl ToSql for Fingerprint {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Text(self.to_string()),
))
}
}
impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
type Error = anyhow::Error;
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> anyhow::Result<Self> {
match fpr {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
sequoia_openpgp::Fingerprint::Invalid(_) =>
Err(anyhow!("invalid fingerprint")),
sequoia_openpgp::Fingerprint::Invalid(_) => Err(anyhow!("invalid fingerprint")),
_ => Err(anyhow!("unknown fingerprint type")),
}
}
@@ -95,13 +130,12 @@ impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ::hex::ToHex;
self.0.write_hex_upper(f)
}
}
impl Serialize for Fingerprint {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
@@ -110,26 +144,26 @@ impl Serialize for Fingerprint {
}
impl<'de> Deserialize<'de> for Fingerprint {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
String::deserialize(deserializer).and_then(|string| {
Self::from_str(&string)
.map_err(|err| Error::custom(err.to_string()))
Self::from_str(&string).map_err(|err| Error::custom(err.to_string()))
})
}
}
impl FromStr for Fingerprint {
type Err = Error;
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Fingerprint> {
fn from_str(s: &str) -> anyhow::Result<Fingerprint> {
match sequoia_openpgp::Fingerprint::from_hex(s)? {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
sequoia_openpgp::Fingerprint::Invalid(_) =>
Err(anyhow!("'{}' is not a valid fingerprint", s)),
sequoia_openpgp::Fingerprint::Invalid(_) => {
Err(anyhow!("'{}' is not a valid fingerprint", s))
}
_ => Err(anyhow!("unknown fingerprint type")),
}
}
@@ -138,15 +172,29 @@ impl FromStr for Fingerprint {
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
pub struct KeyID([u8; 8]);
impl TryFrom<sequoia_openpgp::Fingerprint> for KeyID {
type Error = Error;
impl FromSql for KeyID {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
value
.as_str()
.and_then(|s| Self::from_str(s).map_err(|_| FromSqlError::InvalidType))
}
}
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
impl ToSql for KeyID {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Text(self.to_string()),
))
}
}
impl TryFrom<sequoia_openpgp::Fingerprint> for KeyID {
type Error = anyhow::Error;
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> anyhow::Result<Self> {
match fpr {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a).into()),
sequoia_openpgp::Fingerprint::Invalid(_) => {
Err(anyhow!("invalid fingerprint"))
},
sequoia_openpgp::Fingerprint::Invalid(_) => Err(anyhow!("invalid fingerprint")),
_ => Err(anyhow!("unknown fingerprint type")),
}
}
@@ -172,19 +220,19 @@ impl From<Fingerprint> for KeyID {
impl fmt::Display for KeyID {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ::hex::ToHex;
self.0.write_hex_upper(f)
}
}
impl FromStr for KeyID {
type Err = Error;
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<KeyID> {
fn from_str(s: &str) -> anyhow::Result<KeyID> {
match sequoia_openpgp::KeyID::from_hex(s)? {
sequoia_openpgp::KeyID::V4(a) => Ok(KeyID(a)),
sequoia_openpgp::KeyID::Invalid(_) =>
Err(anyhow!("'{}' is not a valid long key ID", s)),
sequoia_openpgp::KeyID::Invalid(_) => {
Err(anyhow!("'{}' is not a valid long key ID", s))
}
_ => Err(anyhow!("unknown keyid type")),
}
}
@@ -203,10 +251,11 @@ mod tests {
assert_eq!(c("Foo Bar <foo@example.org>").as_str(), "foo@example.org");
// FIXME gotta fix this
// assert_eq!(c("foo@example.org <foo@example.org>").as_str(), "foo@example.org");
assert_eq!(c("\"Foo Bar\" <foo@example.org>").as_str(),
"foo@example.org");
assert_eq!(c("foo@👍.example.org").as_str(),
"foo@xn--yp8h.example.org");
assert_eq!(
c("\"Foo Bar\" <foo@example.org>").as_str(),
"foo@example.org"
);
assert_eq!(c("foo@👍.example.org").as_str(), "foo@xn--yp8h.example.org");
assert_eq!(c("Foo@example.org").as_str(), "foo@example.org");
assert_eq!(c("foo@EXAMPLE.ORG").as_str(), "foo@example.org");
}

View File

@@ -1,11 +1,11 @@
use crate::openpgp::types::HashAlgorithm;
use anyhow::anyhow;
use sequoia_openpgp::types::HashAlgorithm;
use zbase32;
use super::Result;
// cannibalized from
// https://gitlab.com/sequoia-pgp/sequoia/blob/master/net/src/wkd.rs
pub fn encode_wkd(address: impl AsRef<str>) -> Result<(String,String)> {
pub fn encode_wkd(address: impl AsRef<str>) -> anyhow::Result<(String, String)> {
let (local_part, domain) = split_address(address)?;
let local_part_encoded = encode_local_part(local_part);
@@ -13,11 +13,11 @@ pub fn encode_wkd(address: impl AsRef<str>) -> Result<(String,String)> {
Ok((local_part_encoded, domain))
}
fn split_address(email_address: impl AsRef<str>) -> Result<(String,String)> {
fn split_address(email_address: impl AsRef<str>) -> anyhow::Result<(String, String)> {
let email_address = email_address.as_ref();
let v: Vec<&str> = email_address.split('@').collect();
if v.len() != 2 {
Err(anyhow!("Malformed email address".to_owned()))?;
return Err(anyhow!("Malformed email address".to_owned()));
};
// Convert to lowercase without tailoring, i.e. without taking any

43
default.nix Normal file
View File

@@ -0,0 +1,43 @@
{ lib, rustPlatform, sqlite, openssl, gettext, pkg-config, commitShaShort ? "" }:
rustPlatform.buildRustPackage rec {
pname = "hagrid";
version = "2.1.0";
# src = ./.;
# avoid rebuilds when only about pages change
src = builtins.filterSource
(path: type: !(type == "directory" && baseNameOf path == "aboutPages"))
./.;
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"rocket_i18n-0.5.0" = "sha256-EbUE8Z3TQBnDnptl9qWK6JvsACCgP7EXTxcA7pouYbc=";
};
};
postInstall = ''
cp -r dist $out
'';
nativeBuildInputs = [
pkg-config
gettext
];
buildInputs = [
sqlite
openssl
];
COMMIT_SHA_SHORT = commitShaShort;
meta = with lib; {
description = "A verifying keyserver";
homepage = "https://gitlab.com/keys.openpgp.org/hagrid";
license = with licenses; [ gpl3 ];
maintainers = with maintainers; [ valodim ];
platforms = platforms.all;
};
}

BIN
dist/assets/img/koo.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

23
dist/assets/img/koo.svg vendored Normal file
View File

@@ -0,0 +1,23 @@
<svg width="161" height="163" viewBox="0 0 161 163" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="79.6786" cy="81.6786" r="15.6786" fill="#4FA5FC" opacity="0.2"/>
<circle cx="16" cy="17" r="16" fill="#4FA5FC" opacity="0.2"/>
<circle cx="145" cy="146" r="16" fill="#4FA5FC" opacity="0.2"/>
<circle cx="144.5" cy="16.5" r="15.5" fill="#4FA5FC" opacity="0.2"/>
<circle cx="16" cy="81" r="16" fill="#4FA5FC" opacity="0.2"/>
<circle cx="16" cy="146" r="16" fill="#4FA5FC" opacity="0.2"/>
<rect width="32" height="162" rx="16" fill="url(#paint0_linear_2403_93)"/>
<circle cx="79.6786" cy="146.679" r="14.1108" stroke="#4FA5FC" stroke-width="3.13573" opacity="0.2"/>
<circle cx="79.6786" cy="17.6786" r="14.1108" stroke="#4FA5FC" stroke-width="3.13573" opacity="0.2"/>
<circle cx="144.679" cy="81.6786" r="14.1108" stroke="#4FA5FC" stroke-width="3.13573" opacity="0.2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.0032 81.6796C63.9199 77.4791 65.481 73.2521 68.6863 70.0468L133.047 5.68629C139.295 -0.5621 149.426 -0.562094 155.674 5.68629C161.923 11.9347 161.923 22.0653 155.674 28.3137L102.308 81.6802L155.674 135.047C161.923 141.295 161.923 151.426 155.674 157.674C149.426 163.923 139.295 163.923 133.047 157.674L68.6863 93.3137C65.4806 90.108 63.9196 85.8805 64.0032 81.6796Z" fill="url(#paint1_linear_2403_93)"/>
<defs>
<linearGradient id="paint0_linear_2403_93" x1="16" y1="0" x2="16" y2="162" gradientUnits="userSpaceOnUse">
<stop stop-color="#4FA5FC" stop-opacity="0"/>
<stop offset="1" stop-color="#4FA5FC"/>
</linearGradient>
<linearGradient id="paint1_linear_2403_93" x1="116.873" y1="162.373" x2="116.873" y2="1.37259" gradientUnits="userSpaceOnUse">
<stop stop-color="#4FA5FC" stop-opacity="0"/>
<stop offset="1" stop-color="#4FA5FC"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

18
dist/assets/site.css vendored
View File

@@ -4,6 +4,7 @@ body {
font-family: 'Roboto', sans-serif;
font-weight: 300;
color: #050505;
word-wrap: break-word;
}
/* roboto-300 - latin-ext */
@@ -90,7 +91,6 @@ span.brand {
h1 {
padding-bottom: 0.75em;
padding-bottom: 0.75em;
}
.ui p {
@@ -110,6 +110,12 @@ a.brand {
color: #050505;
}
a.brand img {
height: 1.2em;
margin-right: 20px;
vertical-align: text-top;
}
.usage > h2 a, .usage > h2 a:visited {
color: #050505;
}
@@ -184,6 +190,13 @@ abbr {
padding: 0 15%;
}
@media screen and (min-width: 450px) {
.search, .upload, .manage {
display: flex;
padding: 0 15%;
}
}
.manageEmail, .searchTerm, .fileUpload {
flex-grow: 1;
border: 3px solid;
@@ -325,11 +338,10 @@ span.email {
bottom: 0;
right: 0;
left: 0;
width: 100%;
text-align: end;
font-size: 12px;
color: #bbb;
margin: 10px;
margin: 10px 20px;
}
.attribution a {

View File

@@ -1,85 +0,0 @@
{{#> layout }}
<div class="about">
<center><h2>About | <a href="/about/news">News</a> | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
<p>
The <span class="brand">keys.openpgp.org</span> server is a public service for the
distribution and discovery of OpenPGP-compatible keys, commonly
referred to as a "keyserver".
</p>
<p>
<strong>For instructions, see our <a href="/about/usage">usage guide</a>.</strong>
</p>
<h3>How it works</h3>
<p>
An OpenPGP key contains two types of information:
</p>
<ul>
<li><strong>Identity information</strong> describes the parts of
a key that identify its owner, also known as "User IDs".
A User ID typically includes a name and an email address.
</li>
<li><strong>Non-identity information</strong> is all the technical
information about the key itself. This includes the large numbers
used for verifying signatures and encrypting messages.
It also includes metadata like date of creation, some expiration
dates, and revocation status.
</li>
</ul>
<p>
Traditionally, these pieces of information have always been distributed
together. On <span class="brand">keys.openpgp.org</span>, they are
treated differently. While anyone can upload all parts of any OpenPGP key
to <span class="brand">keys.openpgp.org</span>, our keyserver
will only retain and publish certain parts under certain
conditions:
</p>
<p>
Any <strong>non-identity information</strong> will be stored and freely
redistributed, if it passes a cryptographic integrity check.
Anyone can download these parts at any time as they contain only
technical data that can't be used to directly identify a person.
Good OpenPGP software can use <span class="brand">keys.openpgp.org</span>
to keep this information up to date for any key that it knows about.
This helps OpenPGP users maintain secure and reliable communication.
</p>
<p>
The <strong>identity information</strong> in an OpenPGP key
is only distributed with consent.
It contains personal data, and is not strictly necessary for
a key to be used for encryption or signature verification.
Once the owner gives consent by verifying their email address,
the key can be found via search by address.
</p>
<h3 id="community">Community and platform</h3>
<p>
This service is run as a community effort.
You can talk to us in
#hagrid on OFTC IRC,
also reachable as #hagrid:stratum0.org on Matrix.
Of course you can also reach us via email,
at <tt>support at keys dot openpgp dot org</tt>.
The folks who are running this come
from various projects in the OpenPGP ecosystem,
including Sequoia-PGP, OpenKeychain, and Enigmail.
</p>
<p>
Technically,
<span class="brand">keys.openpgp.org</span> runs on the <a href="https://gitlab.com/hagrid-keyserver/hagrid" target="_blank">Hagrid</a> keyserver software,
which is based on <a href="https://sequoia-pgp.org">Sequoia-PGP</a>.
We are running on <a href="https://eclips.is" target="_blank">eclips.is</a>,
a hosting platform focused on Internet Freedom projects,
which is managed by <a href="https://greenhost.net/" target="_blank">Greenhost</a>.
</p>
</div>
{{/layout}}

View File

@@ -1,284 +0,0 @@
{{#> layout }}
<div class="about">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
<p>
Hagrid implements both the legacy HKP interface, as well as our
native interface, VKS.
</p>
<h2>Verifying Keyserver (VKS) Interface</h2>
<p>Hagrid has its own URL scheme to fetch, submit, and verify keys.</p>
<ul>
<li>
<tt>GET /vks/v1/by-fingerprint/&lt;FINGERPRINT&gt;</tt>
<p>
Retrieves the key with the given <tt>Fingerprint</tt>.
The <tt>Fingerprint</tt> may refer to the primary key, or any subkey.
Hexadecimal digits MUST be uppercase,
and MUST NOT be prefixed with <code>0x</code>.
The returned key is ASCII Armored, and has a content-type of <code>application/pgp-keys</code>.
</p>
</li>
<li>
<tt>GET /vks/v1/by-keyid/&lt;KEY-ID&gt;</tt>
<p>
Retrieves the key with the given long <tt>KeyID</tt>.
The <tt>KeyID</tt> may refer to the primary key, or any subkey.
Hexadecimal digits MUST be uppercase,
and MUST NOT be prefixed with <code>0x</code>.
The returned key is ASCII Armored, and has a content-type of <code>application/pgp-keys</code>.
</p>
</li>
<li>
<tt>GET /vks/v1/by-email/&lt;URI-ENCODED EMAIL-ADDRESS&gt;</tt>
<p>
Retrieves the key with the given <tt>Email Address</tt>.
Only exact matches are accepted.
Lookup by email address requires opt-in by the owner of the email address.
The returned key is ASCII Armored, and has a content-type of <code>application/pgp-keys</code>.
</p>
</li>
<li>
<tt>POST /vks/v1/upload</tt>
<p>
A single key may be submitted using a POST request to <tt>/vks/v1/upload</tt>.
The body of the request must be <code>application/json</code>.
The JSON data must contain a single field <code>keytext</code>,
which must contain the keys to submit.
The value of <code>keytext</code> can be formatted in standard OpenPGP ASCII Armor, or base64.
</p>
<p>
The returned JSON data
contains the fields <code>token</code>, <code>key_fpr</code>,
and <code>status</code>.
The <code>token</code> field contains an opaque value,
which can be used to perform <tt>request-verify</tt> requests
(see below).
The <code>key_fpr</code> field contains the fingerprint of the uploaded primary key.
The <code>status</code> token contains a map of email addresses
contained in the key, with one of the values
<code>unpublished</code>,
<code>published</code>,
<code>revoked</code>, or
<code>pending</code>,
indicating the status of this email address.
</p>
<div class="example">
<div>
Example request:
<pre>
{
"keytext": "&lt;ASCII ARMORED KEY&gt;"
}
</pre>
</div>
<div>
Example response:
<pre>
{
"key_fpr": "&lt;FINGERPRINT&gt;",
"status": {
"address@example.org": "unpublished"
},
"token": "..."
}
</pre>
</div>
</div>
</li>
<li>
<tt>POST /vks/v1/request-verify</tt>
<p>
A key that has been uploaded
can be made discoverable by one or more of its email addresses
by proving ownership of the address
via a verification email.
This endpoint requests verification
for one or more email addresses.
</p>
<p>
The body of the request must be <code>application/json</code>.
The JSON data must include the opaque <code>token</code> value
(obtained via <tt>/vks/v1/upload</tt>)
and an <code>addresses</code> field,
which is a list of email addresses (not full User IDs)
to request verification mails for.
It can optionally include a <code>locale</code> field,
which is list of locales,
ordered by preference,
which to use for the verification email.
The reply will be the same as for the <tt>/vks/v1/upload</tt> endpoint,
with addresses marked as <code>pending</code> where a verification email
has been sent.
</p>
<div class="example">
<div>
Example request:
<pre>
{
"token": "...",
"addresses": [
"address@example.org"
],
"locale": [ "de_CH", "de_DE" ]
}
</pre>
</div>
<div>
Example response:
<pre>
{
"key_fpr": "&lt;FINGERPRINT&gt;",
"status": {
"address@example.org": "pending"
},
"token": "..."
}
</pre>
</div>
</div>
</li>
</ul>
<h3>Error handling</h3>
<p>
If a GET request fails for any reason,
a suitable HTTP status code will be returned.
The response will be a plaintext error message.
If a key is not found,
the HTTP status code will be <tt>404</tt>.
</p>
<p>
If a POST request fails for any reason,
a suitable HTTP status code will be returned.
The response body will be
a JSON object
with a single <code>error</code> attribute.
A POST request may fail
with a HTTP 503 error
at any time
if the server is undergoing
database maintenance.
<strong>Clients should handle errors gracefully for POST requests.</strong>
</p>
<div class="example">
<div>
Example response:
<pre>
{
"error": "We are currently undergoing scheduled database maintenance!"
}
</pre>
</div>
</div>
<h3 id="rate-limiting"><a href="#rate-limiting">Rate Limiting</a></h3>
<p>
Requests to the <span class="brand">keys.openpgp.org</span> API are rate
limited:
</p>
<ul>
<li>
Requests by fingerprint or key id are limited to five requests per second.
Excessive requests will fail with <tt>error 429</tt>.
There is a burst window of 1000.
</li>
<li>
Requests by email address are limited to one request per minute.
Excessive requests will fail with <tt>error 429</tt>.
There is a burst window of 50.
</li>
</ul>
<h2>HTTP Keyserver Protocol (HKP) Interface</h2>
<p>
Hagrid implements a subset of
the <a href="https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00">HKP</a>
protocol so that tools like GnuPG and OpenKeychain can use it
without modification.
</p>
<ul>
<li>
<tt>GET /pks/lookup?op=get&amp;options=mr&amp;search=&lt;QUERY&gt;</tt>
<p>Returns an ASCII Armored key matching the query. Query may be:</p>
<ul>
<li>An exact email address query of the form <code>localpart@example.org</code>.</li>
<li>
A hexadecimal representation of a long <tt>KeyID</tt>
(e.g., <code>069C0C348DD82C19</code>, optionally prefixed by <code>0x</code>).
This may be a <tt>KeyID</tt> of either a primary key or a subkey.
</li>
<li>
A hexadecimal representation of a <tt>Fingerprint</tt>
(e.g., <code>8E8C33FA4626337976D97978069C0C348DD82C19</code>, optionally prefixed by <code>0x</code>).
This may be a <tt>Fingerprint</tt> of either a primary key or a subkey.
</li>
</ul>
</li>
<li>
<tt>GET /pks/lookup?op=index&amp;options=mr&amp;search=&lt;QUERY&gt;</tt>
<p>
Returns
a <a href="https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00#section-5.2">machine-readable
list</a> of keys matching the query. Query may have the forms
detailed above. Hagrid always returns either one or no keys at
all.
</p>
</li>
<li>
<tt>POST /pks/add</tt>
<p>
Keys may be submitted using a POST request to <tt>/pks/add</tt>,
the body of the request being
a <code>application/x-www-form-urlencoded</code> query.
<code>keytext</code> must be the keys to submit,
which must be ASCII Armored.
More than one key may be submitted in one request.
For verification of email addresses,
the <tt>/vks/v1/upload</tt> endpoint
must be used instead.
</p>
</li>
</ul>
<a href="#rate-limiting">Rate limiting</a> applies as for the VKS interface
above.
<h4>Limitations</h4>
<p>
By design, Hagrid does not implement the full HKP protocol. The specific
limitations are:
</p>
<ul>
<li>No support for <code>op=vindex</code>.</li>
<li>Only exact matches by email address, fingerprint or long key id are returned.</li>
<li>All requests return either one or no keys.</li>
<li>The expiration date field in <code>op=index</code> is left blank (discussion <a target="_blank" href="https://gitlab.com/hagrid-keyserver/hagrid/issues/134">here</a>).</li>
<li>All parameters and options other than <code>op</code> and <code>search</code> are ignored.</li>
<li>Output is always machine readable (i.e. <code>options=mr</code> is always assumed).</li>
<li>Uploads are restricted to 1 MiB.</li>
<li>All packets that aren't public keys, user IDs or signatures are filtered out.</li>
</ul>
</div>
{{/layout}}

View File

@@ -1,276 +0,0 @@
{{#> layout }}
<div class="about">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | <a href="/about/usage">Usage</a> | FAQ | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
<p>
<strong>For instructions, see our <a href="/about/usage">usage guide</a>.</strong>
</p>
<h3 id="sks-pool"><a href="#sks-pool">Is this server part of the "SKS" pool?</a></h3>
<p>
No. The federation model of the SKS pool has various problems in terms
of reliability, abuse-resistance, privacy, and usability. We might do
something similar to it, but <span class="brand">keys.openpgp.org</span>
will never be part of the SKS pool itself.
</p>
<h3 id="federation"><a href="#federation">Is keys.openpgp.org federated? Can I help by running an instance?</a></h3>
<p>
For the moment, no.
We do plan to decentralize <span class="brand">keys.openpgp.org</span>
at some point.
With multiple servers
run by independent operators,
we can hopefully improve the reliability
of this service even further.
</p>
<p>
Several folks offered to help out
by "running a Hagrid server instance".
We very much appreciate the offer,
but we will probably never have an "open" federation model like SKS,
where everyone can run an instance and become part of a "pool".
This is for two reasons:
</p>
<ol>
<li>
Federation with open participation requires all data to be public.
This significantly impacts the privacy of our users, because it
allows anyone to scrape a list of all email addresses.
</li>
<li>
Servers run as a hobby by casual administrators do not meet our
standards for reliability and performance.
</li>
</ol>
<h3 id="non-email-uids"><a href="#non-email-uids">Why is there no support
for identities that aren't email addresses?</a></h3>
<p>
We require explicit consent to distribute identity information.
Identities that aren't email addresses, such as pictures or website
URLs, offer no simple way for us to acquire this consent.
</p>
<p>
Note: Some OpenPGP software creates keys with incorrectly formatted
email addresses. These addresses might not be recognized correctly on
<span class="brand">keys.openpgp.org</span>.
</p>
<h3 id="verify-multiple"><a href="#verify-multiple">Can I verify more than
one key for some email address?</a></h3>
<p>
An email address can only be associated with a single key.
When an address is verified for a new key,
it will no longer appear in any key
for which it was previously verified.
<a href="/about">Non-identity information</a> will still be distributed
for all keys.
</p>
<p>
This means a search by email address
will only return a single key,
not multiple candidates.
This eliminates an impossible choice for the user
("Which key is the right one?"),
and makes key discovery by email much more convenient.
</p>
<h3 id="email-protection"><a href="#email-protection">What do you do to
protect outgoing verification emails?</a></h3>
<p>
We use a modern standard called
<a href="https://www.hardenize.com/blog/mta-sts" target="_blank">MTA-STS</a>,
combined with
<a href="https://starttls-everywhere.org/" target="_blank">STARTTLS Everywhere</a>
by the EFF,
to make sure verification emails are sent out securely.
This protects against eavesdropping and interception during delivery.
</p>
<p>
The MTA-STS mechanism only works if supported by the recipient's email
provider. Otherwise, emails will be delivered as usual.
You can <a href="https://www.hardenize.com/">run this test</a>
to see if your email provider supports it.
If the "MTA-STS" entry on the left isn't a green checkmark,
please ask your provider to update their configuration.
</p>
<h3 id="third-party-signatures"><a href="#third-party-signatures">
Do you distribute "third party signatures"?</a></h3>
<p>
Short answer: No.
</p>
<p>
A "third party signature" is a signature on a key
that was made by some other key.
Most commonly,
those are the signatures produced when "signing someone's key",
which are the basis for
the "<a href="https://en.wikipedia.org/wiki/Web_of_trust" target="_blank">Web of Trust</a>".
For a number of reasons,
those signatures are not currently distributed
via <span class="brand">keys.openpgp.org</span>.
</p>
<p>
The killer reason is <strong>spam</strong>.
Third party signatures allow attaching arbitrary data to anyone's key,
and nothing stops a malicious user from
attaching so many megabytes of bloat to a key
that it becomes practically unusable.
Even worse,
they could attach offensive or illegal content.
</p>
<p>
There are ideas to resolve this issue.
For example, signatures could be distributed with the signer,
rather than the signee.
Alternatively, we could require
cross-signing by the signee before distribution
to support a
<a href="https://wiki.debian.org/caff" target="_blank">caff-style</a>
workflow.
If there is enough interest,
we are open to working with other OpenPGP projects
on a solution.
</p>
<h3 id="no-sign-verified"><a href="#no-sign-verified">Why not sign keys
after verification?</a></h3>
<p>
The <span class="brand">keys.openpgp.org</span> service is meant for key
distribution and discovery, not as a de facto certification authority.
Client implementations that want to offer verified communication should
rely on their own trust model.
</p>
<h3 id="revoked-uids"><a href="#revoked-uids">Why are revoked identities not
distributed as such?</a></h3>
<p>
When an OpenPGP key marks one of its identities as revoked, this
identity should no longer be considered valid for the key, and this
information should ideally be distributed to all OpenPGP clients that
already know about the newly revoked identity.
</p>
<p>
Unfortunately, there is currently no good way to distribute revocations,
that doesn't also reveal the revoked identity itself. We don't want to
distribute revoked identities, so we can't distribute the identity at
all.
</p>
<p>
There are proposed solutions to this issue, that allow the distribution
of revocations without also revealing the identity itself. But so far
there is no final specification, or support in any OpenPGP software. We
hope that a solution will be established in the near future, and will
add support on <span class="brand">keys.openpgp.org</span> as soon as
we can.
</p>
<h3 id="search-substring"><a href="#search-substring">Why isn't it possible to search by part of an email address, like just the domain?</a></h3>
<p>
Some keyservers support search for keys by part of an email address.
This allows discovery not only of keys, but also of addresses, with a query like "keys for addresses at gmail dot com".
This effectively puts the addresses of all keys on those keyservers into a public listing.
</p>
<p>
A search by email address on <span class="brand">keys.openpgp.org</span> returns a key only if it exactly matches the email address.
That way, a normal user can discover the key associated with any address they already know, but they cannot discover any new email addresses.
This prevents a malicious user or spammer from easily obtaining a list of all email addresses on the server.
</p>
<p>
We made this restriction a part of our <a href="/about/privacy">privacy policy</a>,
which means we can't change it without asking for user consent.
</p>
<h3 id="tor"><a href="#tor">Do you support Tor?</a></h3>
<p>
Of course!
If you have Tor installed,
you can reach <span class="brand">keys.openpgp.org</span> anonymously
as an
<a href="https://support.torproject.org/onionservices/#onionservices-2" target="_blank">onion service</a>:
<br />
<a href="http://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion">zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion</a>
</p>
<h3 id="encrypt-verification-emails"><a href="#encrypt-verification-emails">
Why not encrypt verification emails?</a></h3>
Various reasons:
<ol>
<li>It is more complicated, both for our users and for us.</li>
<li>It doesn't prevent attacks - an attacker gains nothing from
uploading a key they don't have access to.</li>
<li>Deletion would still have to be possible even when a key is
lost.</li>
<li>It would require a different (and more complicated) mechanism to
upload keys that can only sign.</li>
</ol>
<h3 id="older-gnupg"><a href="#older-gnupg">
I have trouble updating some keys with GnuPG. Is there a bug?
</a></h3>
<p>
GnuPG considers keys that contain no identity information to be invalid, and refuses to import them.
However, a key that has no <a href="/about">verified email addresses</a> may still contain useful information.
In particular, it's still possible to check whether the key is revoked or not.
</p>
<p>
In June 2019, the <span class="brand">keys.openpgp.org</span> team created a patch that allows GnuPG to process updates from keys without identity information.
This patch was quickly included in several downstream distributions of GnuPG, including Debian, Fedora, NixOS, and GPG Suite for macOS.
</p>
<p>
In March 2020 the GnuPG team rejected the patch, and updated the issue status to "Wontfix".
This means that <strong>unpatched versions of GnuPG cannot receive updates from <span class="brand">keys.openpgp.org</span> for keys that don't have any verified email address</strong>.
You can read about this decision in issue <a href="https://dev.gnupg.org/T4393#133689">T4393</a> on the GnuPG bug tracker.
</p>
<p>
You can check if your version of GnuPG is affected with the following instructions.
</p>
<blockquote>
<span style="font-size: larger;">Import test key:</span><br>
<br>
$ curl https://keys.openpgp.org/assets/uid-test.pub.asc | gpg --import<br>
gpg: key F231550C4F47E38E: "Alice Lovelace &lt;alice@openpgp.example&gt;" imported<br>
gpg: Total number processed: 1<br>
gpg: imported: 1<br>
<br>
</blockquote>
<blockquote>
<span style="font-size: larger;">With patch, key will be updated if locally known:</span><br>
<br>
$ gpg --recv-keys EB85BB5FA33A75E15E944E63F231550C4F47E38E<br>
gpg: key F231550C4F47E38E: "Alice Lovelace &lt;alice@openpgp.example&gt;" not changed<br>
gpg: Total number processed: 1<br>
gpg: unchanged: 1<br>
<br>
</blockquote>
<blockquote>
<span style="font-size: larger;">Without patch, a key without identity is always rejected:</span><br>
<br>
$ gpg --recv-keys EB85BB5FA33A75E15E944E63F231550C4F47E38E<br>
gpg: key EB85BB5FA33A75E15E944E63F231550C4F47E38E: no user ID<br>
</blockquote>
</div>
{{/layout}}

View File

@@ -1,361 +0,0 @@
{{#> layout }}
<div class="about">
<center><h2><a href="/about">About</a> | News | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
<h2 id="2019-11-12-celebrating-100k">
<div style="float: right; font-size: small; line-height: 2em;">2019-11-12 📅</div>
<a style="color: black;" href="/about/news#2019-11-12-celebrating-100k">Celebrating 100.000 verified addresses! 📈</a>
</h2>
<p>
Five months ago, we launched this service.
And just today, we have reached a remarkable milestone:
<center style="margin-top: 2em; margin-bottom: 2em;">
<img src="/assets/img/stats-addresses-2019-11-12.png" style="padding: 1px; border: 1px solid gray;" /><br />
<strong>One hundred thousand verified email addresses!</strong>
</center>
<p>
Thanks to everyone using this service!
And thanks especially to those who have provided us with feedback,
translations, or even code contributions!
<p>
A few updates on things we've been working on:
<ul>
<li>
This news page is now available as an <strong><a target="_blank" href="/atom.xml">atom feed <img src="/assets/img/atom.svg" style="height: 0.8em;" /></a></strong>.
</li>
<li>
We have been working on
a <strong><a target="_blank" href="https://gitlab.com/hagrid-keyserver/hagrid/issues/131">new mechanism to refresh keys</a></strong>
that better protects the user's privacy.
</li>
<li>
Work on <strong>localization</strong> is in full swing!
we hope to have some languages ready for deployment soon.
</li>
</ul>
<p>
If you would like to see <span class="brand">keys.openpgp.org</span>
translated into your native language,
please <a target="_blank" href="https://www.transifex.com/otf/hagrid/">join the translation team</a>
over on Transifex.
We would appreciate help especially for <strong>Russian</strong>, <strong>Italian</strong>, <strong>Polish</strong> and <strong>Dutch</strong>.
<p>
That's all, keeping this one short!
<span style="font-size: x-large;">👍️</span>
<hr style="margin-top: 2em; margin-bottom: 2em;" />
<h2 id="2019-09-12-three-months-later">
<div style="float: right; font-size: small; line-height: 2em;">2019-09-12 📅</div>
<a style="color: black;" href="/about/news#2019-09-12-three-months-later">Three months after launch ✨</a>
</h2>
<p>
It has been three months now
<a href="/about/news#2019-06-12-launch">since we launched</a>
<span class="brand">keys.openpgp.org</span>.
We are happy to report:
It has been a resounding success!
🥳
<h4>Adoption in clients</h4>
<p>
The
<span class="brand">keys.openpgp.org</span>
keyserver has been received very well by users,
and clients are adopting it rapidly.
It is now used by default in
<a href="https://gpgtools.org/" target="_blank">GPGTools</a>,
<a href="https://enigmail.net/" target="_blank">Enigmail</a>,
<a href="https://www.openkeychain.org/" target="_blank">OpenKeychain</a>,
<a href="https://github.com/firstlookmedia/gpgsync" target="_blank">GPGSync</a>,
Debian,
NixOS,
and others.
Many tutorials have also been updated,
pointing users our way.
<p>
At the time of writing,
more than 70.000 email addresses
have been verified.
<center style="margin-top: 2em; margin-bottom: 2em;">
<img src="/assets/img/stats-addresses-2019-09-12.png" style="padding: 1px; border: 1px solid gray;" /><br />
<span style="font-size: smaller;">If that isn't a promising curve, I don't know what is :)</span>
</center>
<p>
A special shout-out here goes to GPGTools for macOS.
They implemented the update process so smoothly,
the number of verified addresses completely exploded
when they released their update.
<h4>All's good in operations</h4>
<p>
There is not a lot to report operationally,
and no news is good news in this case!
Since launch,
there was nearly zero downtime,
only a single bug came up
that briefly caused issues during upload,
and support volume has been comfortably low.
<p>
Our traffic is currently
at about ten requests per second
(more during the day, less on the weekend),
and we delivered roughly 100.000 emails
in the last month.
No sweat.
<p>
We made several small operational improvements
including deployment of
<a href="http://dnsviz.net/d/keys.openpgp.org/dnssec/" target="_blank">DNSSEC</a>,
implementing some
<a href="/about/api#rate-limiting" target="_blank">rate-limiting</a>,
nailing down our
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">content security policy</a>
headers,
and enabling
<a href="https://blog.torproject.org/whats-new-tor-0298" target="_blank">single-hop</a>
mode on our Tor Onion Service.
You can find a more complete list
<a href="https://gitlab.com/hagrid-keyserver/hagrid/merge_requests?scope=all&utf8=%E2%9C%93&state=merged" target="_blank">here</a>.
<h4>Secure email delivery with MTA-STS</h4>
<p>
One improvement that deserves special mention is
<a href="https://www.hardenize.com/blog/mta-sts">MTA-STS</a>,
which improves the security of outgoing emails.
<p>
While HTTPS is deployed fairly universally these days,
that sadly isn't the case for email.
Many servers don't do encryption at all,
or use a self-signed certificate
instead of a proper one (e.g. from Let's Encrypt).
But delivery failures upset customers more
than reduced security,
and many emails are still delivered without encryption.
<p>
With MTA-STS, domain operators can indicate
(via HTTPS)
that their email server <em>does</em> support encryption.
When a secure connection can't be established
to such a server,
message delivery will be postponed
or eventually bounce,
instead of proceeding insecurely.
<p>
This is extremely useful for service like
<span class="brand">keys.openpgp.org</span>.
If encryption isn't reliable,
attackers can intercept verification emails relatively easily.
But for providers who have MTA-STS deployed,
we can be sure that
every message is delivered securely,
and to the right server.
<p>
You can <a href="https://aykevl.nl/apps/mta-sts/" target="_blank">run a check</a>
to find out whether your email provider
supports MTA-STS.
If they don't,
please drop them a message and tell them
to step up their security game!
<h4>Work in progress</h4>
<p>
We are working on two features:
<p>
The first is <strong>localization</strong>.
Most people do not speak English,
but so far that is the only language we support.
To make this service more accessible,
we are working with the OTF's
<a href="https://www.opentech.fund/labs/localization-lab/" target="_blank">Localization Lab</a>
to make the website and outgoing emails
available in several more languages.
<p>
The second is to bring back
<strong>third-party signatures</strong>.
As <a href="/about/faq#third-party-signatures">mentioned in our FAQ</a>,
we currently don't support these due to spam and potential for abuse.
The idea is to require
<a href="https://gitlab.com/openpgp-wg/rfc4880bis/merge_requests/20/diffs" target="_blank">cross-signatures</a>,
which allow each key to choose for itself
which signatures from other people it wants to distribute.
Despite this extra step,
this is fairly compatible with existing software.
It also nicely stays out of the way of users
who don't care about signatures.
<p>
Although work is in progress for both of those features,
neither have a planned time of release yet.
<p>
Regarding the "<tt>no user ID</tt>" issue with GnuPG
(mentioned in our
<a href="/about/news#2019-06-12-launch-challenges">last news post</a>
and our
<a href="/about/faq#older-gnupg" target="_blank">FAQ</a>),
a patch that fixes this problem is now carried by Debian,
as well as GPGTools for macOS.
GnuPG upstream has not merged the patch so far.
<p>
That's it!
Thanks for your interest!
<span style="font-size: x-large;">👋</span>
<hr style="margin-top: 2em; margin-bottom: 2em;" />
<h2 id="2019-06-12-launch">
<div style="float: right; font-size: small; line-height: 2em;">2019-06-12 📅</div>
<a href="/about/news#2019-06-12-launch" style="color: black;">Launching a new keyserver! 🚀</a>
</h2>
<p>
From a community effort by
<a href="https://enigmail.net" target="_blank">Enigmail</a>,
<a href="https://openkeychain.org" target="_blank">OpenKeychain</a>,
and <a href="https://sequoia-pgp.org">Sequoia PGP</a>,
we are pleased to announce
the launch of the new public OpenPGP keyserver
<span class="brand">keys.openpgp.org</span>!
Hurray! 🎉
<h4>Give me the short story!</h4>
<ul>
<li>Fast and reliable. No wait times, no downtimes, no inconsistencies.</li>
<li>Precise. Searches return only a single key, which allows for easy key discovery.</li>
<li>Validating. Identities are only published with consent,
while non-identity information is freely distributed.</li>
<li>Deletable. Users can delete personal information with a simple email confirmation.</li>
<li>Built on Rust, powered by <a href="https://sequoia-pgp.org" target="_blank">Sequoia PGP</a> - free and open source, running AGPLv3.</li>
</ul>
Get started right now by <a href="/upload">uploading your key</a>!
<h4>Why a new keyserver?</h4>
<p>
We created <span class="brand">keys.openpgp.org</span>
to provide an alternative to the SKS Keyserver pool,
which is the default in many applications today.
This distributed network of keyservers has been struggling with
<a target="_blank" href="https://medium.com/@mdrahony/are-sks-keyservers-safe-do-we-need-them-7056b495101c">abuse</a>,
<a target="_blank" href="https://en.wikipedia.org/wiki/Key_server_(cryptographic)#Problems_with_keyservers">performance</a>,
as well as <a href="http://www.openwall.com/lists/oss-security/2017/12/10/1">privacy issues</a>,
and more recently also
<a target="_blank" href="http://nongnu.13855.n7.nabble.com/SKS-apocalypse-mitigation-td228252.html">GDPR</a>
compliance questions.
Kristian Fiskerstrand has done a stellar job maintaining the pool for
<a target="_blank" href="https://blog.sumptuouscapital.com/2016/12/10-year-anniversary-for-sks-keyservers-net/">more than ten years</a>,
but at this point development activity seems to have
<a target="_blank" href="https://bitbucket.org/skskeyserver/sks-keyserver/pull-requests/60/clean-build-with-405">mostly ceased</a>.
<p>
We thought it time to consider a fresh approach to solve these problems.
<h4>Identity and non-identity information</h4>
<p>
The <span class="brand">keys.openpgp.org</span> keyserver splits up
identity and non-identity information in keys.
You can find more details on our <a href="/about" target="_blank">about page</a>:
The gist is that non-identity information (keys, revocations, and so on)
is freely distributed,
while identity information
is only distributed with consent
that can also be revoked at any time.
<p>
If a new key is verified for some email address,
it will replace the previous one.
This way,
every email address is only associated with a single key at most.
It can also be removed from the listing
at any time by the owner of the address.
This is very useful for key discovery:
if a search by email address returns a key,
it means this is the single key
that is currently valid for the searched email address.
<h4>Support in Enigmail and OpenKeychain</h4>
<p>
The <span class="brand">keys.openpgp.org</span> keysever
will receive first-party support in upcoming releases of
<a href="https://enigmail.net" target="_blank">Enigmail</a> for Thunderbird,
as well as
<a href="https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain&hl=en">OpenKeychain</a> on Android.
This means users of those implementations will
benefit from the faster response times,
and improved key discovery by email address.
We hope that this will also give us some momentum
to build this project into a bigger community effort.
<h4 id="2019-06-12-launch-challenges">Current challenges</h4>
<p>
Privacy-preserving techniques in keyservers are still new,
and sadly there are still a few compatibility issues
caused by splitting out identity information.
<p>
In particular, when GnuPG (as of this writing, version 2.2.16) encounters
an OpenPGP key without identities,
it throws an error "no user ID"
and does not process new non-identity information
(like revocation certificates)
even if it is cryptographically valid.
We are actively engaged in
providing fixes for these issues.
<h4>The future</h4>
<p>
Privacy-preserving techniques in keyservers are still new,
and we have more ideas for reducing the metadata.
But for now, our plan is only to
keep <span class="brand">keys.openpgp.org</span> reliable and fast 🐇,
fix any upcoming bugs 🐞,
and <a href="/about#community">listen to feedback</a> from the community. 👂
<p>
For more info, head on over to
our <a target="_blank" href="/about">about page</a>
and <a target="_blank" href="/about/faq">FAQ</a> pages.
You can get started right away
by <a href="/upload" target="_blank">uploading your your key</a>!
Beyond that there is more cool stuff to discover,
like our <a target="_blank" href="/about/api">API</a>,
and an <a target="_blank" href="/about/faq#tor">Onion Service</a>!
<p>
Cheers!
<span style="font-size: x-large;">🍻</span>
</div>
{{/layout}}

View File

@@ -1,68 +0,0 @@
{{#> layout }}
<div class="about">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | Privacy</h2></center>
<p style="text-align: left;">
The public keyserver running on keys.openpgp.org processes, stores and
distributes OpenPGP key data. The specific way in which data is processed
differs by type as follows:
<ul>
<li><b>Email Addresses</b>
<p>Email addresses contained in <abbr title="Packet Tag 13">User
IDs</abbr> are personally identifiable information (PII).
Special care is taken to make sure they are used only with
consent:
<ul>
<li>
Publishing requires <a target="_blank"
href="https://en.wikipedia.org/wiki/Opt-in_email#Confirmed_opt-in_(COI)_/_Double_opt-in_(DOI)">double
opt-in</a> validation, to prove ownership of the
email address in question.
</li>
<li>Addresses are searchable by exact email address,
but not by associated name.</li>
<li>Enumeration of addresses is not possible.</li>
<li>Deletion of addresses is possible via simple proof
of ownership in an automated fashion, similar to
publication. To unlist an address where this isn't
possible, write to support at keys dot openpgp dot
org.
</li>
</ul>
</p>
<p>This data is never handed collectively ("as a dump") to third
parties.
</p>
</li>
<li><b>Public Key Data</b>
<p>The cryptographic content of OpenPGP keys is not considered personally
identifiable information. This includes specifically
<abbr title="Packet Tags 6 and 14">public key material</abbr>,
<abbr title="Packet Tag 2, Signature types 0x10-0x13, 0x18, 0x19, 0x1F">self-signatures</abbr>, and
<abbr title="Packet Tag 2, Signature types 0x20, 0x28, 0x30">revocation signatures</abbr>.
</p>
<p>This data is not usually collectively available ("as
a dump"), but may be handed upon request to third
parties for purposes of development or research.
</p>
</li>
<li><b>Other User ID data</b>
<p>An OpenPGP key may contain personal data other than email
addresses, such as <abbr title="Packet Tag 13">User IDs</abbr>
that do not contain email addresses, or <abbr
title="Packet Tag 17">image attributes</abbr>. This data is stripped
during upload and never stored, processed, or distributed in
any way.
</p>
<p>OpenPGP packet types that were not specifically mentioned above are
stripped during upload and never stored, processed or
distributed in any way.
</p>
</li>
</ul>
<p style="text-align: left">Data is never relayed to third parties outside of
what is available from the <a href="/about/api">public API interfaces</a>,
and what is described in this policy.
</p>
</div>
{{/layout}}

View File

@@ -1,33 +0,0 @@
{{#> layout }}
<div class="about">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | Stats | <a href="/about/privacy">Privacy</a></h2></center>
<h3>Verified email addresses</h3>
<p>
A simple statistic of the total number of email addresses that are currently verified. 📈
</p>
<p>
<center><img src="/about/stats/month.png" /></center>
</p>
<p>
<center><img src="/about/stats/year.png" /></center>
</p>
<h3>Load Average</h3>
<p>
The "load average" of a server is a statistic of how busy it is. Simply put:
<ul>
<li>0.0 means the <span class="brand">keys.openpgp.org</span> host is completely idle</li>
<li>1.0 is fairly busy</li>
<li>4.0 and above means it's on fire 🔥</li>
</ul>
</p>
<p>
<center><img src="/about/stats/load_week.png" /></center>
</p>
</div>
{{/layout}}

View File

@@ -1,223 +0,0 @@
{{#> layout }}
<div class="about usage">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | Usage | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
<p>
On this page, we collect information on how to use
<span class="brand">keys.openpgp.org</span> with different OpenPGP
software products.<br />
We are still in the process of adding more. If you are missing some, please
write to us and we'll try to add it.
</p>
<h2 id="web" style="padding-left: 3%;">
<a href="#web">Web Interface</a>
</h2>
<p>
The web interface on <span class="brand">keys.openpgp.org</span> allows you to:
</p>
<p>
<ul>
<li><a href="/">Search</a> for keys manually, by fingerprint or email address.</li>
<li><a href="/upload">Upload</a> keys manually, and verify them after upload.</li>
<li><a href="/manage">Manage</a> your keys, and remove published identities.</li>
</ul>
</p>
<h2 id="enigmail">
<div><img src="/assets/img/enigmail.svg"></div>
<a href="#enigmail">Enigmail</a>
</h2>
<p>
<a href="https://enigmail.net" target="_blank">Enigmail</a> for Thunderbird
uses <span class="brand">keys.openpgp.org</span> by default since
version 2.0.12.
</p>
<p>Full support is available since Enigmail 2.1
(for <a href="https://www.thunderbird.net/en-US/thunderbird/68.0beta/releasenotes/" target="_blank">Thunderbird 68</a> or newer):
<ul>
<li>Keys will be kept up to date automatically.</li>
<li>During key creation, you can upload and verify your key.</li>
<li>Keys can be discovered by email address.</li>
</ul>
</p>
<h2 id="gpg-suite">
<div><img src="/assets/img/gpgtools.png"></div>
<a href="#gpg-suite">GPG Suite</a>
</h2>
<p>
<a href="https://gpgtools.org/">GPG Suite</a> for macOS
uses <span class="brand">keys.openpgp.org</span> by default
since August 2019.
</p>
<h2 id="openkeychain">
<div><img src="/assets/img/openkeychain.svg"></div>
<a href="#openkeychain">OpenKeychain</a>
</h2>
<p>
<a href="https://www.openkeychain.org/">OpenKeychain</a> for Android
uses <span class="brand">keys.openpgp.org</span> by default
since July 2019.
<ul>
<li>Keys will be kept up to date automatically.</li>
<li>Keys can be discovered by email address.</li>
</ul>
</p>
<p>
Note that there is no built-in support for upload and email address verification so far.
</p>
<h2 id="pignus">
<div><img src="/assets/img/pignus.png"></div>
<a href="#pignus">Pignus</a>
</h2>
<p>
<a href="https://www.frobese.de/pignus/">Pignus</a> for iOS
uses <span class="brand">keys.openpgp.org</span> by default
since November 2019.
<ul>
<li>Your keys can be uploaded at any time.</li>
<li>Keys can be discovered by email address.</li>
</ul>
</p>
<h2 id="gnupg">
<div><img src="/assets/img/gnupg.svg" /></div>
<a href="#gnupg">GnuPG</a>
</h2>
<p>
To configure <a href="https://gnupg.org">GnuPG</a>
to use <span class="brand">keys.openpgp.org</span> as keyserver,
add this line to your <tt>gpg.conf</tt> file:
<blockquote>
keyserver hkps://keys.openpgp.org
</blockquote>
</p>
<h4 id="gnupg-retrieve"><a href="#gnupg-retrieve">Retrieving keys</a></h4>
<ul>
<li>
To locate the key of a user, by email address:
<blockquote>gpg --auto-key-locate keyserver --locate-keys user@example.net</blockquote>
</li>
<li>To refresh all your keys (e.g. new revocation certificates and subkeys):
<blockquote>gpg --refresh-keys</blockquote>
</li>
</ul>
<h4 id="gnupg-upload"><a href="#gnupg-upload">Uploading your key</a></h4>
<p>
Keys can be uploaded with GnuPG's <tt>--send-keys</tt> command, but
identity information can't be verified that way to make the key
searchable by email address (<a href="/about">what does this mean?</a>).
</p>
<ul>
<li>
You can try this shortcut for uploading your key, which outputs
a direct link to the verification page:
<blockquote>
gpg --export your_address@example.net | curl -T - {{ base_uri }}
</blockquote>
</li>
<li>
Alternatively, you can export them to a file
and select that file in the <a href="/upload" target="_blank">upload</a> page:
<blockquote>
gpg --export your_address@example.net &gt; my_key.pub
</blockquote>
</li>
</ul>
<h4 id="gnupg-troubleshooting"><a href="#gnupg-troubleshooting">Troubleshooting</a></h4>
<ul>
<li>
Some old <tt>~/gnupg/dirmngr.conf</tt> files contain a line like this:
<blockquote>
hkp-cacert ~/.gnupg/sks-keyservers.netCA.pem
</blockquote>
<p>
This configuration is no longer necessary,
but prevents regular certificates from working.
It is recommended to simply remove this line from the configuration.
</p>
</li>
<li>
While refreshing keys, you may see errors like the following:
<blockquote>gpg: key A2604867523C7ED8: no user ID</blockquote>
This is a <a href="https://dev.gnupg.org/T4393" target="_blank">known problem in GnuPG</a>.
We are working with the GnuPG team to resolve this issue.
</li>
</ul>
<h4 id="gnupg-tor"><a href="#gnupg-tor">Usage via Tor</a></h4>
<p>
For users who want to be extra careful,
<span class="brand">keys.openpgp.org</span> can be reached anonymously as an
<a href="https://support.torproject.org/onionservices/#onionservices-2" target="_blank">onion service</a>.
If you have
<a href="https://www.torproject.org/" target="_blank">Tor</a>
installed, use the following configuration:
<blockquote>
keyserver hkp://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion
</blockquote>
</p>
<h2 style="padding-left: 3%;" id="wkd-as-a-service">
<a href="#wkd-as-a-service">WKD as a Service</a>
</h2>
<p> The Web Key Directory (WKD) is a standard for discovery of OpenPGP keys by email address, via the domain of its email provider.
It is used to discover unknown keys in some email clients, such as <a href="https://www.gpg4win.de/about.html" target="_blank">GpgOL</a>.
<p> <span class="brand">keys.openpgp.org</span> can be used as a managed WKD service for any domain.
To do so, the domain simply needs a <tt>CNAME</tt> record that delegates its <tt>openpgpkey</tt> subdomain to <tt>wkd.keys.openpgp.org</tt>.
It should be possible to do this in the web interface of any DNS hoster.
<p> Once enabled for a domain, its verified addresses will automatically be available for lookup via WKD.
<p> The <tt>CNAME</tt> record should look like this:
<blockquote>
$ drill openpgpkey.example.org<br>
...<br>
openpgpkey.example.org. 300 IN CNAME wkd.keys.openpgp.org.
</blockquote>
<p> There is a simple status checker for testing the service:
<blockquote>
$ curl 'https://wkd.keys.openpgp.org/status/?domain=openpgpkey.example.org'<br>
CNAME lookup ok: openpgpkey.example.org resolves to wkd.keys.openpgp.org<br>
</blockquote>
<p> For testing key retrieval:
<blockquote>
$ gpg --locate-keys --auto-key-locate clear,nodefault,wkd address@example.org<br>
</blockquote>
<h2 style="padding-left: 3%;">API</h2>
<p>
We offer an API for integrated support in OpenPGP applications. Check
out our <a href="/about/api">API documentation</a>.
</p>
<h2 style="padding-left: 3%;">Others</h2>
<p>
Missing a guide for your favorite implementation? This site is
a work-in-progress, and we are looking to improve it. Drop us a line at
<span class="email">support at keys dot openpgp dot org</span> if you
want to help out!
</p>
</div>
{{/layout}}

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>keys.openpgp.org</title>
<link href="{{ base_uri }}/atom.xml" rel="self"/>
<id>urn:uuid:8e783366-73b1-460e-83d3-42f01046646d</id>
<updated>2019-11-12T12:00:00Z</updated>
<entry>
<title>Celebrating 100.000 verified addresses! 📈</title>
<link href="{{ base_uri }}/about/news#2019-11-12-celebrating-100k" />
<updated>2019-11-12T12:00:00Z</updated>
<id>urn:uuid:5b69781f-5aa4-4276-8d9e-6a71c896cb65</id>
</entry>
<entry>
<title>Launching a new keyserver! 🚀</title>
<link href="{{ base_uri }}/about/news#2019-06-12-launch" />
<updated>2019-06-12T12:00:00Z</updated>
<id>urn:uuid:a071a6dc-f8ea-43de-b853-bd6d8bbe063f</id>
</entry>
<entry>
<title>Three months after launch ✨</title>
<link href="{{ base_uri }}/about/news#2019-09-12-three-months-later" />
<updated>2019-09-12T12:00:00Z</updated>
<id>urn:uuid:1bd5412a-f480-4c3f-a72d-c9b7a849f544</id>
</entry>
</feed>

View File

@@ -25,7 +25,7 @@
<hr />
<p>
<strong>{{ text "News:" }}</strong> {{ text "<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 verified addresses! 📈</a> (2019-11-12)" }}
<strong>{{ text "News:" }}</strong> {{ text "<a href=\"/about/news#2023-04-28-governance\">keys.openpgp.org governance 📜</a> (2023-04-28)" }}
</p>
{{/with}}
{{/layout}}

View File

@@ -4,19 +4,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/assets/site.css?v=19" type="text/css"/>
<link rel="alternate" href="/atom.xml" type="application/atom+xml" title="keys.openpgp.org newsfeed" />
<link rel="icon" href="/assets/img/koo.ico" />
<title>keys.openpgp.org</title>
</head>
<body lang="{{lang}}">
<div class="card">
<h1><a class="brand" href="/">keys.openpgp.org</a></h1>
<h1><a class="brand" href="/"><img src="/assets/img/koo.svg" alt="">keys.openpgp.org</a></h1>
{{> @partial-block }}
<div class="spacer"></div>
</div>
<div class="attribution">
<p>
<a href="https://gitlab.com/hagrid-keyserver/hagrid/">Hagrid</a>
<a href="https://gitlab.com/keys.openpgp.org/hagrid/">Hagrid</a>
{{ text "v{{ version }} built from" rerender }}
<a href="https://gitlab.com/hagrid-keyserver/hagrid/commit/{{ commit }}">{{ commit }}</a>
<a href="https://gitlab.com/keys.openpgp.org/hagrid/commit/{{ commit }}">{{ commit }}</a>
</p>
<p>{{ text "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>" }}</p>
<p>{{ text "Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0" }}</p>

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1750005367,
"narHash": "sha256-h/aac1dGLhS3qpaD2aZt25NdKY7b+JT0ZIP2WuGsJMU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6c64dabd3aa85e0c02ef1cdcb6e1213de64baee3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

28
flake.nix Normal file
View File

@@ -0,0 +1,28 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils }:
utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages."${system}";
commitShaShort = if self ? rev then (pkgs.lib.substring 0 10 self.rev) else self.dirtyShortRev;
in rec {
packages.hagrid = pkgs.callPackage ./. { inherit commitShaShort; };
packages.hagridctl = pkgs.callPackage ./hagridctl.nix { };
packages.wkdDomainChecker = pkgs.callPackage ./wkd-domain-checker/. { };
packages.default = packages.hagrid;
packages.hagridAboutPages = pkgs.callPackage ./aboutPages { };
}) // {
overlays.hagrid = (final: prev: {
hagrid = self.packages."${final.system}".hagrid;
hagridctl = self.packages."${final.system}".hagridctl;
hagridAboutPages = self.packages."${final.system}".hagridAboutPages;
});
overlays.wkdDomainChecker = (final: prev: { wkdDomainChecker = self.packages."${final.system}".wkdDomainChecker; });
overlays.default = self.overlays.hagrid;
};
}

View File

@@ -12,23 +12,8 @@ limit_req_status 429;
# See https://gitlab.com/sequoia-pgp/hagrid/issues/94
error_page 502 =500 /502;
location /502 {
return 500;
}
# for x-accel-redirect forwards
location /keys {
internal;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
}
location /vks/v1/upload {
proxy_pass http://127.0.0.1:8080;
}
location /vks/v1/request-verify {
proxy_pass http://127.0.0.1:8080;
return 500;
}
location /vks {
@@ -36,12 +21,12 @@ location /vks {
limit_req zone=search_fpr_keyid burst=1000 nodelay;
error_page 404 /errors-static/404-by-fpr.htm;
default_type application/pgp-keys;
add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
# default_type application/pgp-keys;
# add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
try_files /keys/links/by-fpr/$1/$2/$3 =404;
proxy_pass http://127.0.0.1:8080;
}
location ~ ^/vks/v1/by-keyid/(?:0x)?([^/][^/])([^/][^/])(.*)$ {
@@ -49,12 +34,12 @@ location /vks {
error_page 429 /errors-static/429-rate-limit-vks-fpr.htm;
error_page 404 /errors-static/404-by-keyid.htm;
default_type application/pgp-keys;
add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
# default_type application/pgp-keys;
# add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
try_files /keys/links/by-keyid/$1/$2/$3 =404;
proxy_pass http://127.0.0.1:8080;
}
location /vks/v1/by-email/ {
@@ -110,12 +95,12 @@ location /.well-known/openpgpkey {
error_page 429 /errors-static/429-rate-limit-vks-email.htm;
error_page 404 /errors-static/404-wkd.htm;
default_type application/octet-stream;
add_header Content-Disposition 'attachment; filename="$2$3$4.asc"';
# default_type application/octet-stream;
# add_header Content-Disposition 'attachment; filename="$2$3$4.asc"';
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
try_files /keys/links/wkd/$1/$2/$3/$4 =404;
proxy_pass http://127.0.0.1:8080;
}
location ~ "^/.well-known/openpgpkey/([^/]+)/policy$" {
@@ -227,26 +212,6 @@ location /search {
proxy_pass http://127.0.0.1:8080;
}
location /pks {
proxy_pass http://127.0.0.1:8080;
}
location /manage {
proxy_pass http://127.0.0.1:8080;
}
location /verify {
proxy_pass http://127.0.0.1:8080;
}
location /upload {
proxy_pass http://127.0.0.1:8080;
}
location /debug {
proxy_pass http://127.0.0.1:8080;
}
# explicitly cache the home directory
location = / {
proxy_cache static_cache;
@@ -261,6 +226,11 @@ location = /atom.xml {
# cache "about" pages
location /about {
proxy_cache static_cache;
# Should contain output of aboutPages zola build as "about" directory
root /var/www/hagridAboutPages;
}
# all other locations are handled by hagrid
location / {
proxy_pass http://127.0.0.1:8080;
}

36
hagridctl.nix Normal file
View File

@@ -0,0 +1,36 @@
{ lib, rustPlatform, sqlite, openssl, gettext, pkg-config, commitShaShort ? "" }:
rustPlatform.buildRustPackage rec {
pname = "hagridctl";
version = "0.1.0";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"rocket_i18n-0.5.0" = "sha256-EbUE8Z3TQBnDnptl9qWK6JvsACCgP7EXTxcA7pouYbc=";
};
};
nativeBuildInputs = [
pkg-config
gettext
];
buildInputs = [
sqlite
openssl
];
buildAndTestSubdir = "hagridctl";
COMMIT_SHA_SHORT = commitShaShort;
meta = with lib; {
description = "A verifying keyserver";
homepage = "https://gitlab.com/keys.openpgp.org/hagrid";
license = with licenses; [ gpl3 ];
maintainers = with maintainers; [ valodim ];
platforms = platforms.all;
};
}

View File

@@ -2,26 +2,28 @@
name = "hagridctl"
version = "0.1.0"
authors = ["Vincent Breitmoser <look@my.amazin.horse>"]
edition = "2024"
description = "Control hagrid database externally"
[dependencies]
hagrid-database = { path = "../database" }
anyhow = "1"
sequoia-openpgp = { version = "1.3", default-features = false, features = ["crypto-nettle"] }
multipart = "0"
log = "0"
rand = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
time = "0.1"
tempfile = "3.0"
url = "1.6"
hex = "0.3"
base64 = "0.10"
pathdiff = "0.1"
idna = "0.1"
fs2 = "0.4"
walkdir = "2.2"
clap = "2"
toml = "0.5.0"
indicatif = "0.11.0"
hagrid-database = { workspace = true }
anyhow = { workspace = true }
sequoia-openpgp = { workspace = true, features = ["crypto-openssl"] }
multipart = { workspace = true }
log = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_derive = { workspace = true }
serde_json = { workspace = true }
time = { workspace = true }
tempfile = { workspace = true }
url = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
pathdiff = { workspace = true }
idna = { workspace = true }
fs2 = { workspace = true }
walkdir = { workspace = true }
clap = { workspace = true, features = ["derive", "unicode", "env"] }
toml = { workspace = true }
indicatif = { workspace = true }

View File

@@ -1,41 +0,0 @@
[global]
address = "0.0.0.0"
port = 8080
[development]
base-URI = "http://localhost:8080"
from = "noreply@localhost"
x-accel-redirect = false
token_secret = "hagrid"
token_validity = 3600
template_dir = "dist/templates"
assets_dir = "dist/assets"
keys_internal_dir = "state/keys-internal"
keys_external_dir = "state/keys-external"
token_dir = "state/tokens"
tmp_dir = "state/tmp"
maintenance_file = "state/maintenance"
[staging]
base-URI = "https://keys.openpgp.org"
from = "noreply@keys.openpgp.org"
x-accel-redirect = true
template_dir = "templates"
keys_internal_dir = "keys"
keys_external_dir = "public/keys"
assets_dir = "public/assets"
token_dir = "tokens"
tmp_dir = "tmp"
maintenance_file = "maintenance"
[production]
base-URI = "https://keys.openpgp.org"
from = "noreply@keys.openpgp.org"
x-accel-redirect = true
template_dir = "templates"
keys_internal_dir = "keys"
keys_external_dir = "public/keys"
assets_dir = "public/assets"
token_dir = "tokens"
tmp_dir = "tmp"
maintenance_file = "maintenance"

73
hagridctl/src/cli.rs Normal file
View File

@@ -0,0 +1,73 @@
use crate::{delete, import};
use clap::{Parser, Subcommand};
use hagrid_database::Query;
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, about, long_about = None, help_expected = true)]
pub(crate) struct Cli {
#[arg(long, required = false, env = "HAGRID_DB_FILE_PATH")]
/// Set a path to the Sqlite database file
db_file_path: PathBuf,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Delete (address, key)-binding(s), and/or a key(s).
Delete {
#[arg(long)]
/// Also, delete all bindings
all_bindings: bool,
#[arg(long)]
/// Also, delete all bindings and the key
all: bool,
/// E-Mail address, Fingerprint, or KeyID of the TPK to delete.
/// If a Fingerprint or KeyID is given, --all is implied.
query: Query,
},
/// Import keys into Hagrid
Import {
#[arg(required = true)]
/// List of keyring files to import
keyring_files: Vec<PathBuf>,
},
}
pub(crate) fn dispatch_cmd(cli: &Cli) -> anyhow::Result<()> {
let db_file_path = cli.db_file_path.canonicalize()?;
match &cli.command {
Command::Delete {
query,
all_bindings,
all,
} => delete::run(db_file_path, query, *all_bindings, *all),
Command::Import { keyring_files } => import::run(db_file_path, keyring_files.to_owned()),
}
}
pub(crate) fn print_errors(e: anyhow::Error) {
eprint!("{}", e);
let mut cause = e.source();
while let Some(c) = cause {
eprint!(":\n {}", c);
cause = c.source();
}
eprintln!();
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_cli() {
Cli::command().debug_assert()
}
}

87
hagridctl/src/delete.rs Normal file
View File

@@ -0,0 +1,87 @@
use hagrid_database::types::Fingerprint;
use hagrid_database::{Database, Query, Sqlite};
use std::convert::TryInto;
use std::path::Path;
pub(crate) fn run(
db_file_path: impl AsRef<Path>,
query: &Query,
all_bindings: bool,
mut all: bool,
) -> anyhow::Result<()> {
let db = &Sqlite::new_file(
&db_file_path,
Sqlite::log_dir_path_from_db_file_path(&db_file_path)?,
)?;
match query {
Query::ByFingerprint(_) | Query::ByKeyID(_) => {
eprintln!(
"Fingerprint or KeyID given, deleting key and all \
bindings."
);
all = true;
}
_ => (),
}
let tpk = db
.lookup(query)?
.ok_or_else(|| anyhow::format_err!("No TPK matching {:?}", query))?;
let fp: Fingerprint = tpk.fingerprint().try_into()?;
let mut results = Vec::new();
// First, delete the bindings.
if all_bindings || all {
results.push(("all bindings".into(), db.set_email_unpublished_all(&fp)));
} else if let Query::ByEmail(email) = query {
results.push((email.to_string(), db.set_email_unpublished(&fp, email)));
} else {
unreachable!()
}
// Now delete the key(s) itself.
if all {
// TODO
/*for skb in tpk.subkeys() {
results.push(
(skb.subkey().fingerprint().to_keyid().to_string(),
db.unlink_kid(&skb.subkey().fingerprint().try_into()?,
&fp)));
results.push(
(skb.subkey().fingerprint().to_string(),
db.unlink_fpr(&skb.subkey().fingerprint().try_into()?,
&fp)));
}
results.push(
(tpk.fingerprint().to_keyid().to_string(),
db.unlink_kid(&tpk.fingerprint().try_into()?,
&fp)));
results.push(
(tpk.fingerprint().to_string(),
db.update(&fp, None)));
*/
}
let mut err = Ok(());
for (slug, result) in results {
eprintln!(
"{}: {}",
slug,
if let Err(ref e) = result {
e.to_string()
} else {
"Deleted".into()
}
);
if err.is_ok() {
if let Err(e) = result {
err = Err(e);
}
}
}
err
}

View File

@@ -1,31 +1,25 @@
use std::path::{Path,PathBuf};
use std::cmp::min;
use std::convert::TryInto;
use std::fs::File;
use std::io::Read;
use std::thread;
use std::cmp::min;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use anyhow::Result;
use sequoia_openpgp::Packet;
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
extern crate tempfile;
use hagrid_database::{Database, EmailAddressStatus, ImportResult, Sqlite};
extern crate sequoia_openpgp as openpgp;
use openpgp::Packet;
use openpgp::parse::{PacketParser, PacketParserResult, Parse};
extern crate hagrid_database as database;
use database::{Database, KeyDatabase, ImportResult};
use indicatif::{MultiProgress,ProgressBar,ProgressStyle};
use HagridConfig;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
// parsing TPKs takes time, so we benefit from some parallelism. however, the
// database is locked during the entire merge operation, so we get diminishing
// returns after the first few threads.
const NUM_THREADS_MAX: usize = 3;
pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>) -> Result<()> {
#[allow(clippy::needless_collect)]
pub fn run(db_file_path: impl AsRef<Path>, input_files: Vec<PathBuf>) -> anyhow::Result<()> {
let num_threads = min(NUM_THREADS_MAX, input_files.len());
let input_file_chunks = setup_chunks(input_files, num_threads);
@@ -35,11 +29,10 @@ pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>
let threads: Vec<_> = input_file_chunks
.into_iter()
.map(|input_file_chunk| {
let config = config.clone();
let db_file_path = db_file_path.as_ref().to_owned();
let multi_progress = multi_progress.clone();
thread::spawn(move || {
import_from_files(
&config, dry_run, input_file_chunk, multi_progress).unwrap();
import_from_files(db_file_path, input_file_chunk, multi_progress).unwrap();
})
})
.collect();
@@ -53,15 +46,12 @@ pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>
Ok(())
}
fn setup_chunks(
mut input_files: Vec<PathBuf>,
num_threads: usize,
) -> Vec<Vec<PathBuf>> {
let chunk_size = (input_files.len() + (num_threads - 1)) / num_threads;
fn setup_chunks(mut input_files: Vec<PathBuf>, num_threads: usize) -> Vec<Vec<PathBuf>> {
let chunk_size = input_files.len().div_ceil(num_threads);
(0..num_threads)
.map(|_| {
let len = input_files.len();
input_files.drain(0..min(chunk_size,len)).collect()
input_files.drain(0..min(chunk_size, len)).collect()
})
.collect()
}
@@ -76,7 +66,7 @@ struct ImportStats<'a> {
count_unchanged: u64,
}
impl <'a> ImportStats<'a> {
impl<'a> ImportStats<'a> {
fn new(progress: &'a ProgressBar, filename: String) -> Self {
ImportStats {
progress,
@@ -89,7 +79,7 @@ impl <'a> ImportStats<'a> {
}
}
fn update(&mut self, result: Result<ImportResult>) {
fn update(&mut self, result: anyhow::Result<ImportResult>) {
// If a new TPK starts, parse and import.
self.count_total += 1;
match result {
@@ -106,23 +96,25 @@ impl <'a> ImportStats<'a> {
return;
}
self.progress.set_message(&format!(
"{}, imported {:5} keys, {:5} New {:5} Updated {:5} Unchanged {:5} Errors",
&self.filename, self.count_total, self.count_new, self.count_updated, self.count_unchanged, self.count_err));
"{}, imported {:5} keys, {:5} New {:5} Updated {:5} Unchanged {:5} Errors",
&self.filename,
self.count_total,
self.count_new,
self.count_updated,
self.count_unchanged,
self.count_err
));
}
}
fn import_from_files(
config: &HagridConfig,
dry_run: bool,
db_file_path: impl AsRef<Path>,
input_files: Vec<PathBuf>,
multi_progress: Arc<MultiProgress>,
) -> Result<()> {
let db = KeyDatabase::new_internal(
config.keys_internal_dir.as_ref().unwrap(),
config.keys_external_dir.as_ref().unwrap(),
config.tmp_dir.as_ref().unwrap(),
dry_run,
) -> anyhow::Result<()> {
let db = Sqlite::new_file(
&db_file_path,
Sqlite::log_dir_path_from_db_file_path(&db_file_path)?,
)?;
for input_file in input_files {
@@ -132,15 +124,20 @@ fn import_from_files(
Ok(())
}
fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgress) -> Result<()> {
fn import_from_file(
db: &Sqlite,
input: &Path,
multi_progress: &MultiProgress,
) -> anyhow::Result<()> {
let input_file = File::open(input)?;
let bytes_total = input_file.metadata()?.len();
let progress_bar = multi_progress.add(ProgressBar::new(bytes_total));
progress_bar
.set_style(ProgressStyle::default_bar()
progress_bar.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}")
.progress_chars("##-"));
.progress_chars("##-"),
);
progress_bar.set_message("Starting…");
let input_reader = &mut progress_bar.wrap_read(input_file);
@@ -149,16 +146,33 @@ fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgre
read_file_to_tpks(input_reader, &mut |acc| {
let primary_key = acc[0].clone();
let result = import_key(&db, acc);
let key_fpr = match primary_key {
Packet::PublicKey(key) => key.fingerprint(),
Packet::SecretKey(key) => key.fingerprint(),
_ => return,
};
let result = import_key(db, acc);
if let Ok(ref result) = result {
let tpk_status = result.as_tpk_status();
if !tpk_status.is_revoked {
for (email, status) in &tpk_status.email_status {
if status == &EmailAddressStatus::NotPublished {
db.set_email_published(&key_fpr.clone().try_into().unwrap(), email)
.unwrap();
}
}
}
}
if let Err(ref e) = result {
let key_fpr = match primary_key {
Packet::PublicKey(key) => key.fingerprint().to_hex(),
Packet::SecretKey(key) => key.fingerprint().to_hex(),
_ => "Unknown".to_owned(),
};
let error = format!("{}:{:05}:{}: {}\n{}", filename, stats.count_total,
key_fpr, e.to_string(), e.backtrace());
let error = format!(
"{}:{:05}:{}: {}",
filename,
stats.count_total,
key_fpr.to_hex(),
e
);
progress_bar.println(error);
return;
}
stats.update(result);
})?;
@@ -169,8 +183,8 @@ fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgre
fn read_file_to_tpks(
reader: impl Read + Send + Sync,
callback: &mut impl FnMut(Vec<Packet>) -> ()
) -> Result<()> {
callback: &mut impl FnMut(Vec<Packet>),
) -> anyhow::Result<()> {
let mut ppr = PacketParser::from_reader(reader)?;
let mut acc = Vec::new();
@@ -183,7 +197,7 @@ fn read_file_to_tpks(
if !acc.is_empty() {
if let Packet::PublicKey(_) | Packet::SecretKey(_) = packet {
callback(acc);
acc = vec!();
acc = vec![];
}
}
@@ -193,52 +207,6 @@ fn read_file_to_tpks(
Ok(())
}
fn import_key(db: &KeyDatabase, packets: Vec<Packet>) -> Result<ImportResult> {
openpgp::Cert::from_packets(packets.into_iter())
.and_then(|tpk| {
db.merge(tpk)
})
fn import_key(db: &Sqlite, packets: Vec<Packet>) -> anyhow::Result<ImportResult> {
sequoia_openpgp::Cert::from_packets(packets.into_iter()).and_then(|tpk| db.merge(tpk))
}
/*
#[cfg(test)]
mod import_tests {
use std::fs::File;
use tempfile::tempdir;
use openpgp::serialize::Serialize;
use super::*;
#[test]
fn import() {
let root = tempdir().unwrap();
let db = KeyDatabase::new_from_base(root.path().to_path_buf()).unwrap();
// Generate a key and import it.
let (tpk, _) = openpgp::tpk::TPKBuilder::autocrypt(
None, Some("foo@invalid.example.com".into()))
.generate().unwrap();
let import_me = root.path().join("import-me");
tpk.serialize(&mut File::create(&import_me).unwrap()).unwrap();
do_import(root.path().to_path_buf(), vec![import_me]).unwrap();
let check = |query: &str| {
let tpk_ = db.lookup(&query.parse().unwrap()).unwrap().unwrap();
assert_eq!(tpk.fingerprint(), tpk_.fingerprint());
assert_eq!(tpk.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>(),
tpk_.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>());
assert_eq!(tpk_.userids().count(), 0);
};
check(&format!("{}", tpk.primary().fingerprint()));
check(&format!("{}", tpk.primary().fingerprint().to_keyid()));
check(&format!("{}", tpk.subkeys().nth(0).unwrap().subkey()
.fingerprint()));
check(&format!("{}", tpk.subkeys().nth(0).unwrap().subkey()
.fingerprint().to_keyid()));
}
}
*/

View File

@@ -1,104 +1,13 @@
#![feature(proc_macro_hygiene, plugin, decl_macro)]
#![recursion_limit = "1024"]
extern crate anyhow;
extern crate clap;
extern crate tempfile;
extern crate sequoia_openpgp as openpgp;
extern crate hagrid_database as database;
#[macro_use]
extern crate serde_derive;
extern crate toml;
extern crate indicatif;
extern crate walkdir;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::Result;
use clap::{Arg, App, SubCommand};
use clap::Parser;
mod cli;
mod delete;
mod import;
mod regenerate;
#[derive(Deserialize)]
pub struct HagridConfigs {
development: HagridConfig,
staging: HagridConfig,
production: HagridConfig,
}
const ERROR_EXIT_CODE: i32 = 2;
// this is not an exact match - Rocket config has more complicated semantics
// than a plain toml file.
// see also https://github.com/SergioBenitez/Rocket/issues/228
#[derive(Deserialize,Clone)]
pub struct HagridConfig {
_template_dir: Option<PathBuf>,
keys_internal_dir: Option<PathBuf>,
keys_external_dir: Option<PathBuf>,
_assets_dir: Option<PathBuf>,
_token_dir: Option<PathBuf>,
tmp_dir: Option<PathBuf>,
_maintenance_file: Option<PathBuf>,
}
fn main() -> Result<()> {
let matches = App::new("Hagrid Control")
.version("0.1")
.about("Control hagrid database externally")
.arg(Arg::with_name("config")
.short("c")
.long("config")
.value_name("FILE")
.help("Sets a custom config file")
.takes_value(true))
.arg(Arg::with_name("env")
.short("e")
.long("env")
.value_name("ENVIRONMENT")
.takes_value(true)
.default_value("prod")
.possible_values(&["dev","stage","prod"]))
.subcommand(SubCommand::with_name("regenerate")
.about("Regenerate symlink directory"))
.subcommand(SubCommand::with_name("import")
.about("Import keys into Hagrid")
.arg(Arg::with_name("dry run")
.short("n")
.long("dry-run")
.help("don't actually keep imported keys")
)
.arg(Arg::with_name("keyring files")
.required(true)
.multiple(true)))
.get_matches();
let config_file = matches.value_of("config").unwrap_or("Rocket.toml");
let config_data = fs::read_to_string(config_file).unwrap();
let configs: HagridConfigs = toml::from_str(&config_data).unwrap();
let config = match matches.value_of("env").unwrap() {
"dev" => configs.development,
"stage" => configs.staging,
"prod" => configs.production,
_ => configs.development,
fn main() {
let Ok(_) = cli::dispatch_cmd(&cli::Cli::parse()).map_err(cli::print_errors) else {
std::process::exit(ERROR_EXIT_CODE);
};
if let Some(matches) = matches.subcommand_matches("import") {
let dry_run = matches.occurrences_of("dry run") > 0;
let keyrings: Vec<PathBuf> = matches
.values_of_lossy("keyring files")
.unwrap()
.iter()
.map(|arg| PathBuf::from_str(arg).unwrap())
.collect();
import::do_import(&config, dry_run, keyrings)?;
} else if let Some(_matches) = matches.subcommand_matches("regenerate") {
regenerate::do_regenerate(&config)?;
} else {
println!("{}", matches.usage());
}
Ok(())
}

View File

@@ -1,123 +0,0 @@
use anyhow::Result;
use std::path::Path;
use std::time::Instant;
use walkdir::WalkDir;
use indicatif::{ProgressBar,ProgressStyle};
use HagridConfig;
use database::{Database,KeyDatabase,RegenerateResult};
use database::types::Fingerprint;
struct RegenerateStats<'a> {
progress: &'a ProgressBar,
prefix: String,
count_total: u64,
count_err: u64,
count_updated: u64,
count_unchanged: u64,
count_partial: u64,
start_time_partial: Instant,
kps_partial: u64,
}
impl <'a> RegenerateStats<'a> {
fn new(progress: &'a ProgressBar) -> Self {
Self {
progress,
prefix: "".to_owned(),
count_total: 0,
count_err: 0,
count_updated: 0,
count_unchanged: 0,
count_partial: 0,
start_time_partial: Instant::now(),
kps_partial: 0,
}
}
fn update(&mut self, result: Result<RegenerateResult>, fpr: Fingerprint) {
// If a new TPK starts, parse and import.
self.count_total += 1;
self.count_partial += 1;
if (self.count_total % 10) == 0 {
self.prefix = fpr.to_string()[0..4].to_owned();
}
match result {
Err(e) => {
self.progress.println(format!("{}: {}", fpr, e.to_string()));
self.count_err += 1;
},
Ok(RegenerateResult::Updated) => self.count_updated += 1,
Ok(RegenerateResult::Unchanged) => self.count_unchanged += 1,
}
self.progress_update();
}
fn progress_update(&mut self) {
if (self.count_total % 10) != 0 {
return;
}
if self.count_partial >= 1000 {
let runtime = (self.start_time_partial.elapsed().as_millis() + 1) as u64;
self.kps_partial = (self.count_partial * 1000) / runtime;
self.start_time_partial = Instant::now();
self.count_partial = 0;
}
self.progress.set_message(&format!(
"prefix {} regenerated {:5} keys, {:5} Updated {:5} Unchanged {:5} Errors ({:3} keys/s)",
self.prefix, self.count_total, self.count_updated, self.count_unchanged, self.count_err, self.kps_partial));
}
}
pub fn do_regenerate(config: &HagridConfig) -> Result<()> {
let db = KeyDatabase::new_internal(
config.keys_internal_dir.as_ref().unwrap(),
config.keys_external_dir.as_ref().unwrap(),
config.tmp_dir.as_ref().unwrap(),
false,
)?;
let published_dir = config.keys_external_dir.as_ref().unwrap().join("links").join("by-email");
let dirs: Vec<_> = WalkDir::new(published_dir)
.min_depth(1)
.max_depth(1)
.sort_by(|a,b| a.file_name().cmp(b.file_name()))
.into_iter()
.flatten()
.map(|entry| entry.into_path())
.collect();
let progress_bar = ProgressBar::new(dirs.len() as u64);
progress_bar
.set_style(ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}")
.progress_chars("##-"));
let mut stats = RegenerateStats::new(&progress_bar);
for dir in dirs {
progress_bar.inc(1);
regenerate_dir_recursively(&db, &mut stats, &dir)?;
}
progress_bar.finish();
Ok(())
}
fn regenerate_dir_recursively(db: &KeyDatabase, stats: &mut RegenerateStats, dir: &Path) -> Result<()> {
for path in WalkDir::new(dir)
.follow_links(true)
.into_iter()
.flatten()
.filter(|e| e.file_type().is_file())
.map(|entry| entry.into_path()) {
let fpr = KeyDatabase::path_to_primary(&path).unwrap();
let result = db.regenerate_links(&fpr);
stats.update(result, fpr);
}
Ok(())
}

379
justfile Normal file
View File

@@ -0,0 +1,379 @@
[private]
default:
@just --list
# ----------------Settings----------------
set fallback := true
# ----------------Variables----------------
SQLITE_DB_FILE_PATH := 'state/keys-internal/keys.sqlite'
# Regular expresion for sed cmd to match variants of Rust versions
# Matched Rust versions: 1, 1.90, 1.90.0
SED_RUST_VERSION_REGEX := '[0-9](\.[0-9]+){1,2}'
RUST_MANIFEST_STABLE_TOML_URL := 'https://static.rust-lang.org/dist/channel-rust-stable.toml'
# Hierarchycal path in the file to current stable version
RUST_MANIFEST_STABLE_VERSION_QUERY := 'pkg.rust.version'
# Regex in sed(-r) format
# Parsed string example: "1.90.0 (1159e78c4 2025-09-14)"
RUST_MANIFEST_STABLE_VERSION_PARSE_REGEX := '^"([0-9]+).([0-9]+).([0-9]+) \(([0-9A-Fa-f]+) (([0-9]+)-([0-9]+)-([0-9]+))\)"$'
# GOTCH: See documentation to `rust-stable-version` recipe for explanation on what \1, \2, etc. means.
DEFAULT_RUST_STABLE_VERSION_FORMAT := '\1.\2'
GITLAB_CI_FILE_NAME := '.gitlab-ci.yml'
GITLAB_CI_FILE_PATH := absolute_path(GITLAB_CI_FILE_NAME)
CARGO_FILE_NAME := 'Cargo.toml'
CARGO_FILE_PATH := absolute_path(CARGO_FILE_NAME)
CARGO_RUST_VERSION_QUERY := 'package.rust-version'
CLIPPY_FILE_NAME := 'clippy.toml'
CLIPPY_FILE_PATH := absolute_path(CLIPPY_FILE_NAME)
CLIPPY_RUST_VERSION_QUERY := 'msrv'
TOOLCHAIN_FILE_NAME := 'rust-toolchain.toml'
TOOLCHAIN_FILE_PATH := absolute_path(TOOLCHAIN_FILE_NAME)
TOOLCHAIN_RUST_VERSION_QUERY := 'toolchain.channel'
GIT_BRANCH_NAME_PREFIX := 'upgrade_rust_to_'
# ----------------Recipes----------------
# Perform initial setup of developer's system.
[group('setup')]
init: init-rocket-config
# Copy Rocket's template configuration from Rocket.toml.dist to Rocket.toml. Rocket is Rust web framework. See https://rocket.rs/guide/v0.5/configuration/#configuration
[group('setup')]
init-rocket-config:
#!/usr/bin/env -S bash -euo pipefail
[ ! -f Rocket.toml ] \
&& cp Rocket.toml.dist Rocket.toml \
&& echo "Rocket.toml.dist copied to Rocket.toml" \
|| echo "Rocket.toml exists already!"
# Format justfile
[group('fmt')]
[group('format')]
[group('just')]
just-fmt:
just --unstable --fmt
# Format Rust code in all packages (aka path based dependencies)
[group('fmt')]
[group('format')]
cargo-fmt:
cargo fmt --all
# Format all code
[group('fmt')]
[group('format')]
fmt: just-fmt cargo-fmt
alias f := fmt
# Check justfile formatting
[group('fmt')]
[group('just')]
[group('lint')]
just-lint-fmt:
just --unstable --fmt --check
# Check Rust code formatting in all packages (aka path based dependencies)
[group('fmt')]
[group('lint')]
cargo-lint-fmt:
cargo fmt --all -- --check
# Check formatting of all code
[group('fmt')]
[group('lint')]
lint-fmt: just-lint-fmt cargo-lint-fmt
alias lf := lint-fmt
# Lint Rust code with Clippy
[group('clippy')]
[group('lint')]
clippy-lint:
cargo clippy --tests --no-deps --workspace
alias cl := clippy-lint
# Lint all code
[group('lint')]
lint: lint-fmt clippy-lint
alias l := lint
# Fix compilation warnings by applying compiler suggestions
[group('fix')]
cargo-fix *args:
cargo fix --workspace {{ args }}
# Apply Clippy's lint suggestions, i.e. fix Clippy linting warnings or errors
[group('clippy')]
[group('fix')]
clippy-fix *args:
cargo clippy --fix --tests --no-deps --workspace {{ args }}
# Fix lint and compilation warnings and errors. Pass given arguments to all sub-recipes, i.e. `just fix --allow-dirty` calls `just cargo-fix --allow-dirty` and `just clippy-fix --allow-dirty`.
[group('fix')]
fix *args: (cargo-fix args) (clippy-fix args)
# Check Rust code errors
[group('compile')]
check:
cargo check
alias c := check
# Compile all Rust code
[group('compile')]
build *args='--workspace':
cargo build {{ args }}
alias b := build
# Run all tests (i.e. --workspace), but when args given pass them to `cargo test`, e.g. `just test fs::tests::init`
[group('test')]
test args='--workspace':
cargo test {{ args }}
alias t := test
# Run continuous check of Rust code errors. Detect file changes and repeat check automatically. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N).
[group('compile')]
[group('watch')]
watch-check *args:
cargo watch --ignore *.pot {{ args }}
alias wc := watch-check
# Run web server and automatically restart on changes. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N).
[group('compile')]
[group('run')]
[group('watch')]
watch-run *args:
cargo watch --exec 'run --bin hagrid' --ignore *.pot {{ args }}
alias wr := watch-run
# Run tests every time files changed. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N).
[group('test')]
[group('watch')]
watch-test *args:
cargo watch --exec 'test --workspace' --ignore *.pot {{ args }}
alias wt := watch-test
# Run web server
[group('run')]
run:
cargo run
alias r := run
alias run-hagrid := run
alias hagrid := run
# Run "hagridctl" which automate some operations working with database externally, e.g. import keys
[group('run')]
run-hagridctl *args:
cargo run --package hagridctl -- {{ args }}
alias hagridctl := run-hagridctl
# Run "tester" which allows to seed database with sample data, e.g. for testing
[group('run')]
run-tester *args:
cargo run --package tester -- {{ args }}
alias tester := run-tester
# Clean compilation artifacts (i.e. "target" directory)
[group('clean')]
clean:
cargo clean
# Clean changes to translation files
[group('clean')]
[group('translation')]
clean-translations:
git restore po/
# Open database prompt
[group('database')]
@db:
command -v sqlite3 \
&& echo "See sqlite3 CLI Documentation: https://sqlite.org/cli.html\n" \
&& sqlite3 {{ SQLITE_DB_FILE_PATH }} \
|| echo "sqlite3 command has not been found. Please, install it using system's package manager or refer to documentation https://sqlite.org/cli.html for installation." >&2
# Upgrade Rust to a given version, by default current stable Rust version is used
[group('housekeeping')]
upgrade-rust version=`just _rust-stable-version`: _ensure-no-vcs-changes && _upgrade-rust-fixes-reminder
#!/usr/bin/env -S bash -euo pipefail
readonly OLD_VERSIONS=$( \
for recipe in _current-ci-rust-version _current-cargo-rust-version _current-clippy-rust-version _current-toolchain-rust-version; do \
just $recipe; \
done \
| sort -u \
); \
just _upgrade-rust-git-create-branch "{{ replace(version, '.', '_') }}" "{{ GIT_BRANCH_NAME_PREFIX + replace(version, '.', '_') }}"
for recipe in _upgrade-rust-ci _upgrade-rust-cargo _upgrade-rust-clippy _upgrade-rust-toolchain; do \
just $recipe '{{ version }}'; \
done \
just _upgrade-rust-git-commit \
"${OLD_VERSIONS//$'\n'/, }" \
"{{ version }}" \
"{{ GITLAB_CI_FILE_PATH }} {{ CARGO_FILE_PATH }} {{ CLIPPY_FILE_PATH }} {{ TOOLCHAIN_FILE_PATH }}" \
"{{ GITLAB_CI_FILE_NAME }} {{ CARGO_FILE_NAME }} {{ CLIPPY_FILE_NAME }} {{ TOOLCHAIN_FILE_NAME }}"
_ensure-no-vcs-changes:
#!/usr/bin/env -S bash -euo pipefail
readonly CHANGED_FILES=$(git ls-files --deleted --modified --others --exclude-standard -- :/); \
git diff-index --quiet HEAD -- >&2 && true
readonly HAS_STAGED_FILES=$?
if [ -n "$CHANGED_FILES" ] || [ $HAS_STAGED_FILES != 0 ]; \
then \
echo -e "{{ RED }}You have working directory changes! \nTo avoid loosing or corrupting your changes commit or stash (git stash -u) them before running commands which change code automatically!{{ NORMAL }}"; \
exit 1; \
fi
_upgrade-rust-git-create-branch branched_version branch_name:
#!/usr/bin/env -S bash -euo pipefail
readonly CURRENT_BRANCH=$(git branch --show-current)
if [[ "$CURRENT_BRANCH" == *{{ branched_version }}* ]]; then
echo "{{ GREEN }}It looks like you switched to new branch manually. Continue...{{ NORMAL }}"
exit 0
fi
while true; do
read -p "Would you like to create new branch ({{ branch_name }})? [y/n]: " input
case "$input" in
y)
break
;;
n)
exit 0
;;
*)
echo "{{ RED }}Incorrect input. Please use only y or n.{{ NORMAL }}"
;;
esac
done
git switch --create "{{ branch_name }}"
_upgrade-rust-fixes-reminder:
#!/usr/bin/env -S bash -euo pipefail
echo -e "\n {{ YELLOW }}Don't forget to fix linting (just lint) and compilation (just check) warnings and errors!{{ NORMAL }}\n"
[confirm('Would you like to commit changes? [y/n]')]
_upgrade-rust-git-commit old_versions new_version file_paths file_names:
#!/usr/bin/env -S bash -euo pipefail
echo "Commiting changes ..."; \
git add {{ file_paths }}; \
sed -r 's/^ {4}//' <<'MSG' | git commit --file -
Upgrade Rust toolchain: {{ old_versions }} -> {{ new_version }}
If you don't have toolchain installed and you use rustup run:
$ rustup toolchain install --profile default --component rustfmt,clippy {{ new_version }}
NOTE: It might be that you have {{ new_version }}.0 installed as stable toolchain, in
that case you still have to run the above command to install exactly {{ new_version }}.
Command: `just upgrade-rust`
Changes:
- Upgrade version of used toolchain in the following places:
{{ ' - ' + replace_regex(file_names, '\s+', "\n - ") }}
MSG
_upgrade-rust version file_path file_name current_version_recipe version_error_msg_part success_msg_part substitude_cmd:
#!/usr/bin/env -S bash -euo pipefail
readonly OLD=$(just {{ current_version_recipe }} {{ file_path }}); \
[ -z "$OLD" ] \
&& ( \
echo "{{ RED }}{{ file_name }}{{ NORMAL }}: Can't determine {{ version_error_msg_part }} before upgrade" >&2; \
exit 1; \
); \
sed -r -i "{{ substitude_cmd }}" {{ file_path }} \
&& ( \
readonly NEW=$(just {{ current_version_recipe }} {{ file_path }}); \
[ -z "NEW" ] \
&& ( \
echo "{{ RED }}{{ file_name }}{{ NORMAL }}: Can't determine {{ version_error_msg_part }} after upgrade" >&2; \
exit 1; \
); \
echo "{{ GREEN }}{{ file_name }}{{ NORMAL }}: {{ success_msg_part }}: {{ BOLD }}$OLD{{ NORMAL }} -> {{ BOLD }}$NEW{{ NORMAL }}"; \
) \
|| echo "{{ RED }}{{ file_name }}{{ NORMAL }}: Upgrade failed" >&2
# Upgrade GitLab CI's Rust to a given version
_upgrade-rust-ci version: (_upgrade-rust version GITLAB_CI_FILE_PATH GITLAB_CI_FILE_NAME '_current-ci-image' 'CI image version' 'image upgraded' 's|image:\s+\"rust:(' + SED_RUST_VERSION_REGEX + ')-([a-z]+)\"\s*$|image: \"rust:' + version + '-\3\"|')
# Upgrade current Rust version in Cargo.toml
_upgrade-rust-cargo version: (_upgrade-rust version CARGO_FILE_PATH CARGO_FILE_NAME '_current-cargo-rust-version' 'Rust version in ' + CARGO_FILE_NAME 'version upgraded' 's|rust-version\s*=\s*\"(' + SED_RUST_VERSION_REGEX + ')\"|rust-version = \"' + version + '\"|')
# Upgrade current Rust version in clippy.toml
_upgrade-rust-clippy version: (_upgrade-rust version CLIPPY_FILE_PATH CLIPPY_FILE_NAME '_current-clippy-rust-version' 'Rust version in ' + CLIPPY_FILE_NAME 'version upgraded' 's|msrv\s*=\s*\"(' + SED_RUST_VERSION_REGEX + ')\"|msrv = \"' + version + '\"|')
# Upgrade current Rust version in rust-toolchain.toml
_upgrade-rust-toolchain version: (_upgrade-rust version TOOLCHAIN_FILE_PATH TOOLCHAIN_FILE_NAME '_current-toolchain-rust-version' 'Rust version in ' + CLIPPY_FILE_NAME 'version upgraded' 's|channel\s*=\s*\"(' + SED_RUST_VERSION_REGEX + ')\"|channel = \"' + version + '\"|')
# Get version of current stable Rust
#
# Parsed string example: "1.90.0 (1159e78c4 2025-09-14)"
# Parsed components:
# \1 - MAJOR
# \2 - MINOR
# \3 - PATCH
# \4 - HASH
# \5 - RELEASE DATE
# \6 - RELEASE YEAR
# \7 - RELEASE MONTH
# \8 - RELEASE DAY
#
# Example of custom format: just rust-stable-version '\5 \4 \1.\2'
# Ouputs: 2025-09-14 1159e78c4 1.90
_rust-stable-version format=DEFAULT_RUST_STABLE_VERSION_FORMAT:
#!/usr/bin/env -S bash -euo pipefail
curl -s {{ RUST_MANIFEST_STABLE_TOML_URL }} \
| tq {{ RUST_MANIFEST_STABLE_VERSION_QUERY }} \
| sed -rn 's|{{ RUST_MANIFEST_STABLE_VERSION_PARSE_REGEX }}|{{ format }}|p'
# Extract current CI image in use
_current-ci-image file_path=GITLAB_CI_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
sed -rn "s|\s*image:\s+\"(rust:({{ SED_RUST_VERSION_REGEX }})-([a-z]+))\"\s*$|\1|p" "{{ file_path }}"
_current-ci-rust-version file_path=GITLAB_CI_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
sed -rn "s|\s*image:\s+\"rust:({{ SED_RUST_VERSION_REGEX }})-([a-z]+)\"\s*$|\1|p" "{{ file_path }}"
# Extract current Rust version from Cargo.toml
_current-cargo-rust-version file_path=CARGO_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
tq --file "{{ file_path }}" --raw {{ CARGO_RUST_VERSION_QUERY }}
# Extract current Rust version from clippy.toml
_current-clippy-rust-version file_path=CLIPPY_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
tq --file "{{ file_path }}" --raw {{ CLIPPY_RUST_VERSION_QUERY }}
# Extract current Rust version from rust-toolchain.toml
_current-toolchain-rust-version file_path=TOOLCHAIN_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
tq --file "{{ file_path }}" --raw {{ TOOLCHAIN_RUST_VERSION_QUERY }}

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env zsh
for i in templates-untranslated/**/*.hbs; do
local template=${${i#templates-untranslated/}}
local prefix_file=${i:h}/template-prefix
local suffix_file=${i:h}/template-suffix
echo -n "$template: "
echo -n "en "
local dist_path=dist/templates/$template
cat $prefix_file $i $suffix_file >! $dist_path
for translated in templates-translated/*/$template(N); do
local locale=${${translated#templates-translated/}%%/*}
local dist_path=dist/templates/localized/$locale/$template
if [[ ! -d ${dist_path:h} ]]; then
mkdir -p ${dist_path:h}
fi
echo -n "$locale "
# echo "cat $prefix_file $translated $suffix_file >! $dist_path"
cat $prefix_file $translated $suffix_file >! $dist_path
done
echo
done

Some files were not shown because too many files have changed in this diff Show More