From ead4ecf755a514c77b67a3f78a09290dd025512d Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 16 Jun 2025 21:35:34 +0200 Subject: [PATCH 1/3] add cffi for deltatouch --- Cargo.lock | 411 +++++++++++++++++- Cargo.toml | 3 +- message_parser_ffi/Cargo.toml | 30 ++ message_parser_ffi/message_parser.h | 103 +++++ message_parser_ffi/readme.md | 8 + message_parser_ffi/smoke_test/main.c | 13 + .../src/bin/generate-headers.rs | 3 + message_parser_ffi/src/lib.rs | 155 +++++++ 8 files changed, 707 insertions(+), 19 deletions(-) create mode 100644 message_parser_ffi/Cargo.toml create mode 100644 message_parser_ffi/message_parser.h create mode 100644 message_parser_ffi/readme.md create mode 100644 message_parser_ffi/smoke_test/main.c create mode 100644 message_parser_ffi/src/bin/generate-headers.rs create mode 100644 message_parser_ffi/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3ac54e6..ac439d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,12 +186,70 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "ext-trait" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5" +dependencies = [ + "ext-trait-proc_macros", +] + +[[package]] +name = "ext-trait-proc_macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "extension-traits" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537" +dependencies = [ + "ext-trait", +] + +[[package]] +name = "extern-c" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320bea982e85d42441eb25c49b41218e7eaa2657e8f90bc4eca7437376751e23" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + [[package]] name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -201,6 +259,25 @@ dependencies = [ "libc", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inventory" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b12ebb6799019b044deaf431eadfe23245b259bba5a2c0796acec3943a3cdb" +dependencies = [ + "rustversion", +] + [[package]] name = "itertools" version = "0.10.5" @@ -233,9 +310,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" [[package]] name = "log" @@ -243,6 +320,22 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "macro_rules_attribute" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" + [[package]] name = "memchr" version = "2.6.3" @@ -264,6 +357,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" +[[package]] +name = "message_parser_ffi" +version = "0.1.0" +dependencies = [ + "deltachat_message_parser", + "quick-xml", + "safer-ffi", + "serde", + "serde_json", + "urlencoding", +] + [[package]] name = "message_parser_wasm" version = "0.14.1" @@ -315,6 +420,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "plotters" version = "0.3.5" @@ -343,24 +454,91 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" -version = "1.0.33" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rayon" version = "1.8.0" @@ -410,12 +588,59 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "safer-ffi" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435fdd58b61a6f1d8545274c1dfa458e905ff68c166e65e294a0130ef5e675bd" +dependencies = [ + "extern-c", + "inventory", + "libc", + "macro_rules_attribute", + "paste", + "safer_ffi-proc_macros", + "scopeguard", + "stabby", + "uninit", + "unwind_safe", + "with_builtin_macros", +] + +[[package]] +name = "safer_ffi-proc_macros" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f25be5ba5f319542edb31925517e0380245ae37df50a9752cdbc05ef948156" +dependencies = [ + "macro_rules_attribute", + "prettyplease", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "same-file" version = "1.0.6" @@ -437,11 +662,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" -version = "1.0.188" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -469,31 +700,84 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "stabby" +version = "36.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b7e94eaf470c2e76b5f15fb2fb49714471a36cc512df5ee231e62e82ec79f8" +dependencies = [ + "rustversion", + "stabby-abi", +] + +[[package]] +name = "stabby-abi" +version = "36.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc7a63b8276b54e51bfffe3d85da56e7906b2dcfcb29018a8ab666c06734c1a" +dependencies = [ + "rustc_version", + "rustversion", + "sha2-const-stable", + "stabby-macros", +] + +[[package]] +name = "stabby-macros" +version = "36.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eecb7ec5611ec93ec79d120fbe55f31bea234dc1bed1001d4a071bb688651615" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "rand", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" -version = "2.0.37" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -519,6 +803,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "unic-idna-punycode" version = "0.9.0" @@ -537,6 +838,27 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "uninit" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e130f2ed46ca5d8ec13c7ff95836827f92f5f5f37fd2b2bf16f33c408d98bb6" +dependencies = [ + "extension-traits", +] + +[[package]] +name = "unwind_safe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "walkdir" version = "2.4.0" @@ -547,6 +869,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -568,7 +896,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.103", "wasm-bindgen-shared", ] @@ -602,7 +930,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -689,3 +1017,52 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "with_builtin_macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59d55032495429b87f9d69954c6c8602e4d3f3e0a747a12dea6b0b23de685da" +dependencies = [ + "with_builtin_macros-proc_macros", +] + +[[package]] +name = "with_builtin_macros-proc_macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bd7679c15e22924f53aee34d4e448c45b674feb6129689af88593e129f8f42" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] diff --git a/Cargo.toml b/Cargo.toml index 3ffde8b..9a56471 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,7 @@ serde_derive = "1.0.126" unic-idna-punycode = "0.9.0" [workspace] -members = ["message_parser_wasm"] - +members = [ "message_parser_ffi","message_parser_wasm"] [dev-dependencies] criterion = "0.3" diff --git a/message_parser_ffi/Cargo.toml b/message_parser_ffi/Cargo.toml new file mode 100644 index 0000000..2518938 --- /dev/null +++ b/message_parser_ffi/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "message_parser_ffi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = [ + "staticlib", # Ensure it gets compiled as a (static) C library + # "cdylib", # If you want a shared/dynamic C library (advanced) + "lib", # For `generate-headers`, `examples/`, `tests/` etc. +] + +[[bin]] +name = "generate-headers" +required-features = ["headers"] # Do not build unless generating headers. + +[dependencies] +deltachat_message_parser = { version = "0.14.1", path = ".." } +quick-xml = "0.37.5" +safer-ffi = "0.1.13" +serde = "1.0.219" +serde_json = "1.0.140" +urlencoding = "2.1.3" + +[features] +# If you want to generate the headers, use a feature-gate +# to opt into doing so: +headers = ["safer-ffi/headers"] diff --git a/message_parser_ffi/message_parser.h b/message_parser_ffi/message_parser.h new file mode 100644 index 0000000..d743cf0 --- /dev/null +++ b/message_parser_ffi/message_parser.h @@ -0,0 +1,103 @@ +/*! \file */ +/******************************************* + * * + * File auto-generated by `::safer_ffi`. * + * * + * Do not manually edit this file. * + * * + *******************************************/ + +#ifndef __RUST_MESSAGE_PARSER_FFI__ +#define __RUST_MESSAGE_PARSER_FFI__ +#ifdef __cplusplus +extern "C" { +#endif + + +#include + +/** */ +typedef struct TextResultForQt { + /** \brief + * whether to use `Text.RichText` instead of the faster `Text.StyledText` + * https://doc.qt.io/archives/qt-5.15/qml-qtquick-text.html#textFormat-prop + */ + bool advanced; + + /** \brief + * text field for `QML Type: Text` https://doc.qt.io/archives/qt-5.15/qml-qtquick-text.html#textFormat-prop + * + * the clickable text is prefixed with a keyword telling you how to handle it: + * - `hashtag`: open search + * - `link` and `labled-link`: open the link - value contains the link destination object as json (so ui knows wether it contains unicode). + * - `email`: offer to start chat with email address + * - `bcs`: bot command suggestion, prefill draft with value + */ + char * html; +} TextResultForQt_t; + +/** \brief + * frees the TextResultForQt + */ +void +free_text_result_for_qt ( + TextResultForQt_t result); + + +#include +#include + +/** \brief + * Modes of the parser, which element set to parse + * + * see https://github.com/deltachat/message-parser/blob/main/spec.md for details + */ +/** \remark Has the same ABI as `uint8_t` **/ +#ifdef DOXYGEN +typedef +#endif +enum ParsingMode { + /** \brief + * Email addresses, Links, Bot command suggestions and hashtags + * + * Basically the text displayed is not changed, just clickable + */ + PARSING_MODE_TEXT, + /** \brief + * The desktop set includes everythin of the text set and additionally: + * - Delimited Email addresses: `` + * - Delimited Links: `` + * - Labeled Links: `[Name](url)` + */ + PARSING_MODE_DESKTOP, + /** \brief + * Desktop set additionally to a markdown subset like code blocks and bold and italics + */ + PARSING_MODE_MARKDOWN, +} +#ifndef DOXYGEN +; typedef uint8_t +#endif +ParsingMode_t; + +/** \brief + * Pretty-prints a TextResultForQt using Rust's formatting logic. + */ +TextResultForQt_t +parse_to_text_result_for_qt ( + char const * text, + ParsingMode_t mode); + +/** \brief + * Pretty-prints a TextResultForQt using Rust's formatting logic. + */ +void +print_text_result_for_qt ( + TextResultForQt_t const * result); + + +#ifdef __cplusplus +} /* extern \"C\" */ +#endif + +#endif /* __RUST_MESSAGE_PARSER_FFI__ */ diff --git a/message_parser_ffi/readme.md b/message_parser_ffi/readme.md new file mode 100644 index 0000000..06787a1 --- /dev/null +++ b/message_parser_ffi/readme.md @@ -0,0 +1,8 @@ +```sh +cc -o ../target/debug/main -L ../target/debug -l message_parser_ffi smoke_test/main.c +../target/debug/main +``` + +``` +cargo run --features headers --bin generate-headers +``` \ No newline at end of file diff --git a/message_parser_ffi/smoke_test/main.c b/message_parser_ffi/smoke_test/main.c new file mode 100644 index 0000000..15cb712 --- /dev/null +++ b/message_parser_ffi/smoke_test/main.c @@ -0,0 +1,13 @@ +#include +// #include + +#include "../message_parser.h" + +int +main (int argc, char const * const argv[]) +{ + char text[] = "hello **world**. go to delta.chat"; + struct TextResultForQt result = parse_to_text_result_for_qt(text, PARSING_MODE_TEXT); + print_text_result_for_qt(&result); + printf("result: %s", result.html); +} diff --git a/message_parser_ffi/src/bin/generate-headers.rs b/message_parser_ffi/src/bin/generate-headers.rs new file mode 100644 index 0000000..221f1ad --- /dev/null +++ b/message_parser_ffi/src/bin/generate-headers.rs @@ -0,0 +1,3 @@ +fn main() -> ::std::io::Result<()> { + ::message_parser_ffi::generate_headers() +} \ No newline at end of file diff --git a/message_parser_ffi/src/lib.rs b/message_parser_ffi/src/lib.rs new file mode 100644 index 0000000..fea4a0a --- /dev/null +++ b/message_parser_ffi/src/lib.rs @@ -0,0 +1,155 @@ +use std::ffi::CString; + +use ::safer_ffi::prelude::*; +use deltachat_message_parser::parser::{Element, LinkDestination}; +use quick_xml::escape::escape; +use safer_ffi::char_p::{char_p_boxed, char_p_ref}; + +// IDEAS +// - returning json representation +// - IDEA return in C types? +// -> though complex enums (sum types) are not supported in safer_ffi yet: +// https://getditto.github.io/safer_ffi/derive-reprc/enum.html#more-complex-enums +// - expose other functions +// - emoji functions + +/// Modes of the parser, which element set to parse +/// +/// see https://github.com/deltachat/message-parser/blob/main/spec.md for details +#[derive_ReprC] // <- `::safer_ffi`'s attribute +#[repr(u8)] // <- explicit integer `repr` is mandatory! +pub enum ParsingMode { + /// Email addresses, Links, Bot command suggestions and hashtags + /// + /// Basically the text displayed is not changed, just clickable + Text, + /// The desktop set includes everythin of the text set and additionally: + /// - Delimited Email addresses: `` + /// - Delimited Links: `` + /// - Labeled Links: `[Name](url)` + Desktop, + /// Desktop set additionally to a markdown subset like code blocks and bold and italics + Markdown, +} + +#[derive_ReprC] +#[repr(C)] +#[derive(Debug, Clone)] +pub struct TextResultForQt { + /// whether to use `Text.RichText` instead of the faster `Text.StyledText` + /// https://doc.qt.io/archives/qt-5.15/qml-qtquick-text.html#textFormat-prop + pub advanced: bool, + /// text field for `QML Type: Text` https://doc.qt.io/archives/qt-5.15/qml-qtquick-text.html#textFormat-prop + /// + /// the clickable text is prefixed with a keyword telling you how to handle it: + /// - `hashtag`: open search + /// - `link` and `labled-link`: open the link - value contains the link destination object as json (so ui knows wether it contains unicode). + /// - `email`: offer to start chat with email address + /// - `bcs`: bot command suggestion, prefill draft with value + pub html: char_p_boxed, +} + +/// Pretty-prints a TextResultForQt using Rust's formatting logic. +#[ffi_export] +pub fn print_text_result_for_qt(result: &TextResultForQt) { + println!("{:?}", result); +} + +/// frees the TextResultForQt +#[ffi_export] +pub fn free_text_result_for_qt(result: TextResultForQt) { + drop(result); +} + +fn encode_link_destination(destination: &LinkDestination) -> String { + let destination_data = serde_json::to_string(&destination).unwrap_or("serdejson-serialization-error".to_string()); + // into username part as uri encoded + format!("{}@link", urlencoding::encode(&destination_data)) +} + +fn element_to_qt_html(input: &Element) -> String { + match input { + Element::Text(text) => escape(*text).to_string(), + Element::Tag(tag) => { + format!(r#""{}""#, escape(*tag), escape(*tag)).to_string() + } + Element::Linebreak => "
".to_string(), + Element::Link { destination } => format!( + r#""{}""#, + encode_link_destination(destination), + escape(destination.target) + ) + .to_string(), + Element::EmailAddress(email) => { + let escaped_email = escape(*email); + format!(r#""{escaped_email}""#).to_string() + } + Element::BotCommandSuggestion(bcs) => { + let escaped_bcs = escape(*bcs); + format!(r#""{escaped_bcs}""#).to_string() + } + + Element::Bold(elements) => { + let element_xml: String = elements.iter().map(|e| element_to_qt_html(e)).collect(); + format!(r#""{element_xml}""#).to_string() + } + Element::Italics(elements) => { + let element_xml: String = elements.iter().map(|e| element_to_qt_html(e)).collect(); + format!(r#""{element_xml}""#).to_string() + } + Element::StrikeThrough(elements) => { + let element_xml: String = elements.iter().map(|e| element_to_qt_html(e)).collect(); + format!(r#""{element_xml}""#).to_string() + } + Element::LabeledLink { label, destination } => { + let label_xml: String = label.iter().map(|e| element_to_qt_html(e)).collect(); + format!( + r#""{label_xml}""#, + encode_link_destination(destination) + ) + .to_string() + } + + Element::InlineCode { content } => { + format!(r#""{}""#, escape(*content)).to_string() + } + Element::CodeBlock { language, content } => format!( + r#""
{}
""#, + language.map(escape).unwrap_or_default(), + escape(*content) + ) + .to_string(), + } +} + +/// Pretty-prints a TextResultForQt using Rust's formatting logic. +#[ffi_export] +pub fn parse_to_text_result_for_qt(text: char_p_ref<'_>, mode: ParsingMode) -> TextResultForQt { + let input = text.to_str(); + let elements = match mode { + ParsingMode::Text => deltachat_message_parser::parser::parse_only_text(input), + ParsingMode::Desktop => deltachat_message_parser::parser::parse_desktop_set(input), + ParsingMode::Markdown => deltachat_message_parser::parser::parse_markdown_text(input), + }; + + // build xml + let html: String = elements.iter().map(|e| element_to_qt_html(e)).collect(); + + // determine what mode is suffient + let advanced = html.contains(""); + + TextResultForQt { + html: char_p_boxed::from( + CString::new(html).unwrap_or(CString::new("message-parser-ffi error").unwrap()), + ), + advanced, + } +} + +// The following function is only necessary for the header generation. +#[cfg(feature = "headers")] // c.f. the `Cargo.toml` section +pub fn generate_headers() -> ::std::io::Result<()> { + ::safer_ffi::headers::builder() + .to_file("message_parser.h")? + .generate() +} From a8181c35fe45680726c08df2c2e52d69810a0ce3 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 16 Jun 2025 22:02:20 +0200 Subject: [PATCH 2/3] add emoji methods to cffi --- message_parser_ffi/message_parser.h | 35 +++++++++++++++++++++++----- message_parser_ffi/smoke_test/main.c | 22 +++++++++++------ message_parser_ffi/src/lib.rs | 31 +++++++++++++++++++++--- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/message_parser_ffi/message_parser.h b/message_parser_ffi/message_parser.h index d743cf0..8639ea0 100644 --- a/message_parser_ffi/message_parser.h +++ b/message_parser_ffi/message_parser.h @@ -14,6 +14,26 @@ extern "C" { #endif +#include +#include + +/** \brief + * Count emojis in a message, if there are only emojis. + * + * This is used to display messages with only emojis in a larger font size. + */ +uint32_t +mp_count_emojis_if_only_contains_emoji ( + char const * text); + +/** \brief + * frees a string managed by rust + */ +void +mp_free_rust_string ( + char * string); + + #include /** */ @@ -40,12 +60,15 @@ typedef struct TextResultForQt { * frees the TextResultForQt */ void -free_text_result_for_qt ( +mp_free_text_result_for_qt ( TextResultForQt_t result); - -#include -#include +/** \brief + * get_first_emoji of text, result needs to be freed with `mp_free_rust_string` + */ +char * +mp_get_first_emoji ( + char const * text); /** \brief * Modes of the parser, which element set to parse @@ -84,7 +107,7 @@ ParsingMode_t; * Pretty-prints a TextResultForQt using Rust's formatting logic. */ TextResultForQt_t -parse_to_text_result_for_qt ( +mp_parse_to_text_result_for_qt ( char const * text, ParsingMode_t mode); @@ -92,7 +115,7 @@ parse_to_text_result_for_qt ( * Pretty-prints a TextResultForQt using Rust's formatting logic. */ void -print_text_result_for_qt ( +mp_print_text_result_for_qt ( TextResultForQt_t const * result); diff --git a/message_parser_ffi/smoke_test/main.c b/message_parser_ffi/smoke_test/main.c index 15cb712..248f2e8 100644 --- a/message_parser_ffi/smoke_test/main.c +++ b/message_parser_ffi/smoke_test/main.c @@ -3,11 +3,19 @@ #include "../message_parser.h" -int -main (int argc, char const * const argv[]) -{ - char text[] = "hello **world**. go to delta.chat"; - struct TextResultForQt result = parse_to_text_result_for_qt(text, PARSING_MODE_TEXT); - print_text_result_for_qt(&result); - printf("result: %s", result.html); +int main(int argc, char const *const argv[]) { + char text[] = "hello **world**. go to delta.chat"; + struct TextResultForQt result = + mp_parse_to_text_result_for_qt(text, PARSING_MODE_TEXT); + mp_print_text_result_for_qt(&result); + printf("\nresult: %s", result.html); + mp_free_text_result_for_qt(result); + + int emoji_count = mp_count_emojis_if_only_contains_emoji( + "🇩🇪😅🧑‍🎨👨‍👩‍👧👩🏽‍🌾"); + printf("\nemoji count: %d, should be 5", emoji_count); + + char *first_emoji = mp_get_first_emoji("👩🏽‍🌾 Farmers"); + printf("\nfirst emoji of the string: \n%s\n", first_emoji); + mp_free_rust_string(first_emoji); } diff --git a/message_parser_ffi/src/lib.rs b/message_parser_ffi/src/lib.rs index fea4a0a..3a0529f 100644 --- a/message_parser_ffi/src/lib.rs +++ b/message_parser_ffi/src/lib.rs @@ -51,13 +51,13 @@ pub struct TextResultForQt { /// Pretty-prints a TextResultForQt using Rust's formatting logic. #[ffi_export] -pub fn print_text_result_for_qt(result: &TextResultForQt) { +pub fn mp_print_text_result_for_qt(result: &TextResultForQt) { println!("{:?}", result); } /// frees the TextResultForQt #[ffi_export] -pub fn free_text_result_for_qt(result: TextResultForQt) { +pub fn mp_free_text_result_for_qt(result: TextResultForQt) { drop(result); } @@ -124,7 +124,7 @@ fn element_to_qt_html(input: &Element) -> String { /// Pretty-prints a TextResultForQt using Rust's formatting logic. #[ffi_export] -pub fn parse_to_text_result_for_qt(text: char_p_ref<'_>, mode: ParsingMode) -> TextResultForQt { +pub fn mp_parse_to_text_result_for_qt(text: char_p_ref<'_>, mode: ParsingMode) -> TextResultForQt { let input = text.to_str(); let elements = match mode { ParsingMode::Text => deltachat_message_parser::parser::parse_only_text(input), @@ -146,6 +146,31 @@ pub fn parse_to_text_result_for_qt(text: char_p_ref<'_>, mode: ParsingMode) -> T } } +/// get_first_emoji of text, result needs to be freed with `mp_free_rust_string` +#[ffi_export] +pub fn mp_get_first_emoji(text: char_p_ref<'_>) -> char_p_boxed { + let input = text.to_str(); + + match deltachat_message_parser::parser::is_emoji::get_first_emoji(input) { + Some(emoji) => char_p_boxed::from(CString::new(emoji).unwrap()), + None => char_p_boxed::from(CString::new("").unwrap()), + } +} + +/// Count emojis in a message, if there are only emojis. +/// +/// This is used to display messages with only emojis in a larger font size. +#[ffi_export] +pub fn mp_count_emojis_if_only_contains_emoji(text: char_p_ref<'_>) -> u32 { + deltachat_message_parser::parser::is_emoji::count_emojis_if_only_contains_emoji(text.to_str()).unwrap_or(0) +} + +/// frees a string managed by rust +#[ffi_export] +pub fn mp_free_rust_string(string: char_p_boxed) { + drop(string); +} + // The following function is only necessary for the header generation. #[cfg(feature = "headers")] // c.f. the `Cargo.toml` section pub fn generate_headers() -> ::std::io::Result<()> { From 1dc54b8690a281ff0ab594af9488cff712f20272 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 16 Jun 2025 22:07:04 +0200 Subject: [PATCH 3/3] cargo fmt --- .../src/bin/generate-headers.rs | 2 +- message_parser_ffi/src/lib.rs | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/message_parser_ffi/src/bin/generate-headers.rs b/message_parser_ffi/src/bin/generate-headers.rs index 221f1ad..6921387 100644 --- a/message_parser_ffi/src/bin/generate-headers.rs +++ b/message_parser_ffi/src/bin/generate-headers.rs @@ -1,3 +1,3 @@ fn main() -> ::std::io::Result<()> { ::message_parser_ffi::generate_headers() -} \ No newline at end of file +} diff --git a/message_parser_ffi/src/lib.rs b/message_parser_ffi/src/lib.rs index 3a0529f..50d1783 100644 --- a/message_parser_ffi/src/lib.rs +++ b/message_parser_ffi/src/lib.rs @@ -62,7 +62,8 @@ pub fn mp_free_text_result_for_qt(result: TextResultForQt) { } fn encode_link_destination(destination: &LinkDestination) -> String { - let destination_data = serde_json::to_string(&destination).unwrap_or("serdejson-serialization-error".to_string()); + let destination_data = + serde_json::to_string(&destination).unwrap_or("serdejson-serialization-error".to_string()); // into username part as uri encoded format!("{}@link", urlencoding::encode(&destination_data)) } @@ -70,9 +71,12 @@ fn encode_link_destination(destination: &LinkDestination) -> String { fn element_to_qt_html(input: &Element) -> String { match input { Element::Text(text) => escape(*text).to_string(), - Element::Tag(tag) => { - format!(r#""{}""#, escape(*tag), escape(*tag)).to_string() - } + Element::Tag(tag) => format!( + r#""{}""#, + escape(*tag), + escape(*tag) + ) + .to_string(), Element::Linebreak => "
".to_string(), Element::Link { destination } => format!( r#""{}""#, @@ -158,11 +162,12 @@ pub fn mp_get_first_emoji(text: char_p_ref<'_>) -> char_p_boxed { } /// Count emojis in a message, if there are only emojis. -/// -/// This is used to display messages with only emojis in a larger font size. +/// +/// This is used to display messages with only emojis in a larger font size. #[ffi_export] pub fn mp_count_emojis_if_only_contains_emoji(text: char_p_ref<'_>) -> u32 { - deltachat_message_parser::parser::is_emoji::count_emojis_if_only_contains_emoji(text.to_str()).unwrap_or(0) + deltachat_message_parser::parser::is_emoji::count_emojis_if_only_contains_emoji(text.to_str()) + .unwrap_or(0) } /// frees a string managed by rust