diff --git a/.gitignore b/.gitignore index 2f22286a56..be4a1fdd92 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,9 @@ node_modules/ /test-results/ /packages/playwright-report/ /packages/playwright/.cache/ - +# Allow geolocation plugin sources to be tracked +/packages/geolocation/android/build/ +/packages/geolocation/android/.gradle/ # Zed .zed/ diff --git a/Cargo.lock b/Cargo.lock index 1d2b8a57f3..db9ca8bc71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,7 +1584,7 @@ dependencies = [ name = "barebones-template-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] @@ -1670,7 +1670,7 @@ version = "0.0.0" dependencies = [ "bevy", "color", - "dioxus", + "dioxus 0.7.1", "dioxus-native", "tracing-subscriber", "wgpu 26.0.1", @@ -3209,7 +3209,7 @@ name = "bluetooth-scanner" version = "0.1.1" dependencies = [ "btleplug", - "dioxus", + "dioxus 0.7.1", "futures", "futures-channel", "tokio", @@ -4258,9 +4258,19 @@ dependencies = [ [[package]] name = "const-serialize" version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd339aa356cc6452308fad2ee56623f900a8e68bc0ab9360a0ddb8270e5640c8" dependencies = [ - "const-serialize", - "const-serialize-macro", + "const-serialize-macro 0.7.1", + "serde", +] + +[[package]] +name = "const-serialize" +version = "0.8.0" +dependencies = [ + "const-serialize 0.8.0", + "const-serialize-macro 0.8.0", "rand 0.9.2", "serde", ] @@ -4268,6 +4278,17 @@ dependencies = [ [[package]] name = "const-serialize-macro" version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "797d158acb331e2a89d696343a27cd39bf7e36aaef33ba4799a5ef1526e24861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "const-serialize-macro" +version = "0.8.0" dependencies = [ "proc-macro2", "quote", @@ -5346,36 +5367,36 @@ name = "dioxus" version = "0.7.1" dependencies = [ "criterion", - "dioxus", - "dioxus-asset-resolver", - "dioxus-cli-config", - "dioxus-config-macro", - "dioxus-config-macros", - "dioxus-core", - "dioxus-core-macro", - "dioxus-desktop", - "dioxus-devtools", - "dioxus-document", - "dioxus-fullstack", - "dioxus-fullstack-macro", - "dioxus-history", - "dioxus-hooks", - "dioxus-html", + "dioxus 0.7.1", + "dioxus-asset-resolver 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-config-macro 0.7.1", + "dioxus-config-macros 0.7.1", + "dioxus-core 0.7.1", + "dioxus-core-macro 0.7.1", + "dioxus-desktop 0.7.1", + "dioxus-devtools 0.7.1", + "dioxus-document 0.7.1", + "dioxus-fullstack 0.7.1", + "dioxus-fullstack-macro 0.7.1", + "dioxus-history 0.7.1", + "dioxus-hooks 0.7.1", + "dioxus-html 0.7.1", "dioxus-liveview", - "dioxus-logger", + "dioxus-logger 0.7.1", "dioxus-native", "dioxus-router", "dioxus-server", - "dioxus-signals", + "dioxus-signals 0.7.1", "dioxus-ssr", - "dioxus-stores", - "dioxus-web", + "dioxus-stores 0.7.1", + "dioxus-web 0.7.1", "env_logger 0.11.8", "futures-util", - "manganis", + "manganis 0.7.1", "rand 0.9.2", "serde", - "subsecond", + "subsecond 0.7.1", "thiserror 2.0.17", "tokio", "tracing", @@ -5383,12 +5404,40 @@ dependencies = [ "wasm-splitter", ] +[[package]] +name = "dioxus" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76e820919058a685a1fdbb2ef4888c73ac77d623c39a7dfde2aa812947246be" +dependencies = [ + "dioxus-asset-resolver 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-config-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-config-macros 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-desktop 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-devtools 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-document 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-fullstack 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-history 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-hooks 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-html 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-stores 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-web 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "manganis 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "subsecond 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "warnings", +] + [[package]] name = "dioxus-asset-resolver" version = "0.7.1" dependencies = [ - "dioxus", - "dioxus-cli-config", + "dioxus 0.7.1", + "dioxus-cli-config 0.7.1", "http 1.3.1", "infer", "jni 0.21.1", @@ -5404,11 +5453,32 @@ dependencies = [ "web-sys", ] +[[package]] +name = "dioxus-asset-resolver" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6a124667ce5565c39fe2f33af45c21fe459c5bfcf7a8074ad12c9e9da5817c" +dependencies = [ + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 1.3.1", + "infer", + "jni 0.21.1", + "js-sys", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "percent-encoding", + "thiserror 2.0.17", + "tokio", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "dioxus-autofmt" version = "0.7.1" dependencies = [ - "dioxus-rsx", + "dioxus-rsx 0.7.1", "pretty_assertions", "prettyplease", "proc-macro2", @@ -5452,24 +5522,26 @@ dependencies = [ "clap", "console 0.16.1", "console-subscriber", - "const-serialize", + "const-serialize 0.7.1", + "const-serialize 0.8.0", "convert_case 0.8.0", "crossterm 0.29.0", "ctrlc", "depinfo", "dioxus-autofmt", "dioxus-check", - "dioxus-cli-config", + "dioxus-cli-config 0.7.1", "dioxus-cli-opt", "dioxus-cli-telemetry", "dioxus-component-manifest", - "dioxus-core", - "dioxus-core-types", - "dioxus-devtools-types", + "dioxus-core 0.7.1", + "dioxus-core-types 0.7.1", + "dioxus-devtools-types 0.7.1", "dioxus-dx-wire-format", - "dioxus-fullstack", - "dioxus-html", - "dioxus-rsx", + "dioxus-fullstack 0.7.1", + "dioxus-html 0.7.1", + "dioxus-platform-bridge", + "dioxus-rsx 0.7.1", "dioxus-rsx-hotreload", "dioxus-rsx-rosetta", "dircpy", @@ -5495,8 +5567,9 @@ dependencies = [ "krates", "local-ip-address", "log", - "manganis", - "manganis-core", + "manganis 0.7.1", + "manganis-core 0.7.1", + "manganis-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "memmap", "memoize", "notify", @@ -5505,6 +5578,7 @@ dependencies = [ "open", "path-absolutize", "pdb", + "permissions", "plist", "posthog-rs", "prettyplease", @@ -5522,7 +5596,7 @@ dependencies = [ "serde_json5", "shell-words", "strum 0.27.2", - "subsecond-types", + "subsecond-types 0.7.1", "syn 2.0.108", "tar", "target-lexicon 0.13.3", @@ -5560,6 +5634,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "dioxus-cli-config" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "babc8eaf90379352bc4820830749fd231feb9312433d4094b4e7b79d912b3d96" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "dioxus-cli-opt" version = "0.7.1" @@ -5568,15 +5651,16 @@ dependencies = [ "browserslist-rs 0.19.0", "built 0.8.0", "codemap", - "const-serialize", + "const-serialize 0.8.0", "grass", "image", "imagequant", "lightningcss", - "manganis", - "manganis-core", + "manganis 0.7.1", + "manganis-core 0.7.1", "mozjpeg", "object 0.37.3", + "permissions", "png", "rayon", "serde", @@ -5612,7 +5696,8 @@ dependencies = [ name = "dioxus-cli-optimization-test" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", + "dioxus 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", ] @@ -5646,9 +5731,25 @@ dependencies = [ "quote", ] +[[package]] +name = "dioxus-config-macro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30018b5b95567cee42febbb444d5e5e47dbe3e91fa6e44b9e571edad0184cd36" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dioxus-config-macros" +version = "0.7.1" + [[package]] name = "dioxus-config-macros" version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a16b25f8761253ed5ffa4d0789376310fbbc1bbaa8190fc2f374db82c6285a1" [[package]] name = "dioxus-core" @@ -5656,13 +5757,13 @@ version = "0.7.1" dependencies = [ "anyhow", "const_format", - "dioxus", - "dioxus-core-types", - "dioxus-html", + "dioxus 0.7.1", + "dioxus-core-types 0.7.1", + "dioxus-html 0.7.1", "dioxus-ssr", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "longest-increasing-subsequence", "pretty_assertions", "rand 0.9.2", @@ -5672,7 +5773,7 @@ dependencies = [ "serde", "slab", "slotmap", - "subsecond", + "subsecond 0.7.1", "sysinfo 0.35.2", "tokio", "tracing", @@ -5682,14 +5783,37 @@ dependencies = [ "web-sys", ] +[[package]] +name = "dioxus-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75468d08468919f783b0f7ee826802f4e8e66c5b5a0451245d861c211ca18216" +dependencies = [ + "anyhow", + "const_format", + "dioxus-core-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "longest-increasing-subsequence", + "rustc-hash 2.1.1", + "rustversion", + "serde", + "slab", + "slotmap", + "subsecond 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "warnings", +] + [[package]] name = "dioxus-core-macro" version = "0.7.1" dependencies = [ "convert_case 0.8.0", - "dioxus", - "dioxus-html", - "dioxus-rsx", + "dioxus 0.7.1", + "dioxus-html 0.7.1", + "dioxus-rsx 0.7.1", "proc-macro2", "quote", "rustversion", @@ -5698,9 +5822,28 @@ dependencies = [ "trybuild", ] +[[package]] +name = "dioxus-core-macro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f145abdb2a3f858456cb4382390863cf0398c228ad0733618f48891da7687be3" +dependencies = [ + "convert_case 0.8.0", + "dioxus-rsx 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dioxus-core-types" +version = "0.7.1" + [[package]] name = "dioxus-core-types" version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f5ecf5a51de06d78aded3b5f7516a258f53117cba718bc5706317a3c04c844" [[package]] name = "dioxus-desktop" @@ -5711,28 +5854,28 @@ dependencies = [ "bytes", "cocoa", "core-foundation 0.10.1", - "dioxus", - "dioxus-asset-resolver", - "dioxus-cli-config", - "dioxus-core", - "dioxus-devtools", - "dioxus-document", - "dioxus-history", - "dioxus-hooks", - "dioxus-html", - "dioxus-interpreter-js", - "dioxus-signals", + "dioxus 0.7.1", + "dioxus-asset-resolver 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-devtools 0.7.1", + "dioxus-document 0.7.1", + "dioxus-history 0.7.1", + "dioxus-hooks 0.7.1", + "dioxus-html 0.7.1", + "dioxus-interpreter-js 0.7.1", + "dioxus-signals 0.7.1", "dioxus-ssr", "dunce", "exitcode", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "global-hotkey", "http-range", "infer", "jni 0.21.1", - "lazy-js-bundle", + "lazy-js-bundle 0.7.1", "libc", "muda", "ndk 0.9.0", @@ -5761,19 +5904,74 @@ dependencies = [ "wry", ] +[[package]] +name = "dioxus-desktop" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f493c74ff09410c5eadf42abd031d4b3d4032a4d5a2411c77d1d0d5156ca3687" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "cocoa", + "core-foundation 0.10.1", + "dioxus-asset-resolver 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-devtools 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-document 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-history 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-hooks 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-html 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-interpreter-js 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dunce", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "global-hotkey", + "infer", + "jni 0.21.1", + "lazy-js-bundle 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "muda", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "objc", + "objc_id", + "percent-encoding", + "rand 0.9.2", + "rfd", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "signal-hook", + "slab", + "subtle", + "tao", + "thiserror 2.0.17", + "tokio", + "tracing", + "tray-icon", + "tungstenite 0.27.0", + "webbrowser", + "wry", +] + [[package]] name = "dioxus-devtools" version = "0.7.1" dependencies = [ - "dioxus-cli-config", - "dioxus-core", - "dioxus-devtools-types", - "dioxus-signals", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-devtools-types 0.7.1", + "dioxus-signals 0.7.1", "futures-channel", "futures-util", "serde", "serde_json", - "subsecond", + "subsecond 0.7.1", "thiserror 2.0.17", "tokio", "tracing", @@ -5781,28 +5979,77 @@ dependencies = [ "warnings", ] +[[package]] +name = "dioxus-devtools" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb2c5019b7fa72e8e6b21ba99e9263bd390c9a30bbf09793b72f4b57ed7c3d7" +dependencies = [ + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-devtools-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "serde_json", + "subsecond 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 2.0.17", + "tracing", + "tungstenite 0.27.0", + "warnings", +] + [[package]] name = "dioxus-devtools-types" version = "0.7.1" dependencies = [ - "dioxus-core", + "dioxus-core 0.7.1", "serde", - "subsecond-types", + "subsecond-types 0.7.1", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b007cec5b8548281921c4e4678926a3936e9d6757e951380685cc6121a6f974" +dependencies = [ + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "subsecond-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "dioxus-document" version = "0.7.1" dependencies = [ - "dioxus", - "dioxus-core", - "dioxus-core-macro", - "dioxus-core-types", - "dioxus-html", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "dioxus-core-macro 0.7.1", + "dioxus-core-types 0.7.1", + "dioxus-html 0.7.1", "futures-channel", "futures-util", - "generational-box", - "lazy-js-bundle", + "generational-box 0.7.1", + "lazy-js-bundle 0.7.1", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "dioxus-document" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c55bcae9aaf150d4a141c61b3826da5a7ac23dfff09726568525cd46336e9a2" +dependencies = [ + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-html 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy-js-bundle 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "tracing", @@ -5813,10 +6060,10 @@ name = "dioxus-dx-wire-format" version = "0.7.1" dependencies = [ "cargo_metadata", - "manganis-core", + "manganis-core 0.7.1", "serde", "serde_json", - "subsecond-types", + "subsecond-types 0.7.1", ] [[package]] @@ -5829,10 +6076,10 @@ dependencies = [ "base64 0.22.1", "bytes", "ciborium", - "dioxus", - "dioxus-html", + "dioxus 0.7.1", + "dioxus-html 0.7.1", "dioxus-ssr", - "dioxus-stores", + "dioxus-stores 0.7.1", "form_urlencoded", "futures", "futures-util", @@ -5884,16 +6131,16 @@ dependencies = [ "const_format", "content_disposition", "derive_more 2.0.1", - "dioxus", - "dioxus-asset-resolver", - "dioxus-cli-config", - "dioxus-core", - "dioxus-fullstack-core", - "dioxus-fullstack-macro", - "dioxus-hooks", - "dioxus-html", + "dioxus 0.7.1", + "dioxus-asset-resolver 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-fullstack-core 0.7.1", + "dioxus-fullstack-macro 0.7.1", + "dioxus-hooks 0.7.1", + "dioxus-html 0.7.1", "dioxus-server", - "dioxus-signals", + "dioxus-signals 0.7.1", "form_urlencoded", "futures", "futures-channel", @@ -5934,6 +6181,63 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "dioxus-fullstack" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff04cef82d6639eb15186f626298645dbd92978bf66dc3efd2e5984a2ff4a1ff" +dependencies = [ + "anyhow", + "async-stream", + "async-tungstenite", + "axum 0.8.6", + "axum-core 0.5.5", + "base64 0.22.1", + "bytes", + "ciborium", + "const-str 0.7.0", + "const_format", + "content_disposition", + "derive_more 2.0.1", + "dioxus-asset-resolver 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-fullstack-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-fullstack-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-hooks 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-html 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "form_urlencoded", + "futures", + "futures-channel", + "futures-util", + "gloo-net", + "headers", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "js-sys", + "mime", + "pin-project", + "reqwest 0.12.24", + "rustversion", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "thiserror 2.0.17", + "tokio-util", + "tracing", + "tungstenite 0.27.0", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + [[package]] name = "dioxus-fullstack-core" version = "0.7.1" @@ -5942,16 +6246,44 @@ dependencies = [ "axum-core 0.5.5", "base64 0.22.1", "ciborium", - "dioxus", - "dioxus-core", - "dioxus-document", - "dioxus-fullstack", - "dioxus-history", - "dioxus-hooks", - "dioxus-signals", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "dioxus-document 0.7.1", + "dioxus-fullstack 0.7.1", + "dioxus-history 0.7.1", + "dioxus-hooks 0.7.1", + "dioxus-signals 0.7.1", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", + "http 1.3.1", + "inventory", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "dioxus-fullstack-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41281c7cd4d311a50933256e19a5d91d0d950ad350dd3232bd4321fdd3a59fb0" +dependencies = [ + "anyhow", + "axum-core 0.5.5", + "base64 0.22.1", + "ciborium", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-document 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-history 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-hooks 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "http 1.3.1", "inventory", "parking_lot", @@ -5969,7 +6301,7 @@ dependencies = [ "axum 0.8.6", "const_format", "convert_case 0.8.0", - "dioxus", + "dioxus 0.7.1", "proc-macro2", "quote", "serde", @@ -5978,12 +6310,51 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "dioxus-fullstack-macro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae73023c8b8fee2692fc50a28063336f0b6930e86727e30c1047c92d30805b49" +dependencies = [ + "const_format", + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "xxhash-rust", +] + +[[package]] +name = "dioxus-geolocation" +version = "0.7.1" +dependencies = [ + "dioxus-mobile-plugin-build", + "dioxus-platform-bridge", + "jni 0.21.1", + "log", + "objc2 0.6.3", + "permissions", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "dioxus-history" version = "0.7.1" dependencies = [ - "dioxus", - "dioxus-core", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "tracing", +] + +[[package]] +name = "dioxus-history" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac73657da5c7a20629482d774b52f4a4f7cb57a520649f1d855d4073e809c98" +dependencies = [ + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "tracing", ] @@ -5991,12 +6362,12 @@ dependencies = [ name = "dioxus-hooks" version = "0.7.1" dependencies = [ - "dioxus", - "dioxus-core", - "dioxus-signals", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "dioxus-signals 0.7.1", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "reqwest 0.12.24", "rustversion", "slab", @@ -6006,29 +6377,46 @@ dependencies = [ "web-sys", ] +[[package]] +name = "dioxus-hooks" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffd445f16d64939e06cd71a1c63a665f383fda6b7882f4c6f8f1bd6efca2046" +dependencies = [ + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustversion", + "slab", + "tracing", + "warnings", +] + [[package]] name = "dioxus-html" version = "0.7.1" dependencies = [ "async-trait", "bytes", - "dioxus", - "dioxus-core", - "dioxus-core-macro", - "dioxus-core-types", - "dioxus-hooks", - "dioxus-html-internal-macro", - "dioxus-rsx", - "dioxus-web", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "dioxus-core-macro 0.7.1", + "dioxus-core-types 0.7.1", + "dioxus-hooks 0.7.1", + "dioxus-html-internal-macro 0.7.1", + "dioxus-rsx 0.7.1", + "dioxus-web 0.7.1", "enumset", "euclid", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "js-sys", "keyboard-types", - "lazy-js-bundle", - "manganis", + "lazy-js-bundle 0.7.1", + "manganis 0.7.1", "rustversion", "serde", "serde_json", @@ -6037,6 +6425,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "dioxus-html" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f407fc73a9554a644872fcccc9faf762acad8f45158e3d67e42ab8dd42f4586" +dependencies = [ + "async-trait", + "bytes", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-hooks 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-html-internal-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "enumset", + "euclid", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "keyboard-types", + "lazy-js-bundle 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustversion", + "serde", + "serde_json", + "serde_repr", + "tracing", +] + [[package]] name = "dioxus-html-internal-macro" version = "0.7.1" @@ -6048,15 +6463,47 @@ dependencies = [ "trybuild", ] +[[package]] +name = "dioxus-html-internal-macro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a968aae4bc92de87cbac3d0d043803b25a7c62c187841e61adcc9b49917c2b2a" +dependencies = [ + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dioxus-interpreter-js" +version = "0.7.1" +dependencies = [ + "dioxus-core 0.7.1", + "dioxus-core-types 0.7.1", + "dioxus-html 0.7.1", + "js-sys", + "lazy-js-bundle 0.7.1", + "rustc-hash 2.1.1", + "serde", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "dioxus-interpreter-js" version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ab170d89308399205f8ad3d43d8d419affe317016b41ca0695186f7593cba2" dependencies = [ - "dioxus-core", - "dioxus-core-types", - "dioxus-html", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-html 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hash 2.1.1", "serde", "sledgehammer_bindgen", @@ -6071,17 +6518,17 @@ name = "dioxus-liveview" version = "0.7.1" dependencies = [ "axum 0.8.6", - "dioxus", - "dioxus-cli-config", - "dioxus-core", - "dioxus-devtools", - "dioxus-document", - "dioxus-history", - "dioxus-html", - "dioxus-interpreter-js", + "dioxus 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-devtools 0.7.1", + "dioxus-document 0.7.1", + "dioxus-history 0.7.1", + "dioxus-html 0.7.1", + "dioxus-interpreter-js 0.7.1", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "rustc-hash 2.1.1", "serde", "serde_json", @@ -6098,13 +6545,29 @@ dependencies = [ name = "dioxus-logger" version = "0.7.1" dependencies = [ - "dioxus", - "dioxus-cli-config", + "dioxus 0.7.1", + "dioxus-cli-config 0.7.1", + "tracing", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "dioxus-logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42237934c6a67f5ed9a8c37e47ca980ee7cfec9e783a9a1f8c2e36c8b96ae74b" +dependencies = [ + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "tracing", "tracing-subscriber", "tracing-wasm", ] +[[package]] +name = "dioxus-mobile-plugin-build" +version = "0.7.1" + [[package]] name = "dioxus-native" version = "0.7.1" @@ -6118,13 +6581,13 @@ dependencies = [ "blitz-paint", "blitz-shell", "blitz-traits", - "dioxus-asset-resolver", - "dioxus-cli-config", - "dioxus-core", - "dioxus-devtools", - "dioxus-document", - "dioxus-history", - "dioxus-html", + "dioxus-asset-resolver 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-devtools 0.7.1", + "dioxus-document 0.7.1", + "dioxus-history 0.7.1", + "dioxus-html 0.7.1", "dioxus-native-dom", "futures-util", "keyboard-types", @@ -6141,34 +6604,48 @@ version = "0.7.1" dependencies = [ "blitz-dom", "blitz-traits", - "dioxus", - "dioxus-core", - "dioxus-html", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "dioxus-html 0.7.1", "futures-util", "keyboard-types", "rustc-hash 2.1.1", "tracing", ] +[[package]] +name = "dioxus-platform-bridge" +version = "0.7.1" +dependencies = [ + "const-serialize 0.8.0", + "const-serialize-macro 0.8.0", + "jni 0.21.1", + "ndk-context", + "objc2 0.6.3", + "permissions", + "platform-bridge-macro", + "thiserror 2.0.17", +] + [[package]] name = "dioxus-playwright-default-features-disabled-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "dioxus-playwright-fullstack-error-codes-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "dioxus-playwright-fullstack-errors-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde", "tokio", ] @@ -6177,7 +6654,7 @@ dependencies = [ name = "dioxus-playwright-fullstack-hydration-order-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde", "tokio", ] @@ -6186,7 +6663,7 @@ dependencies = [ name = "dioxus-playwright-fullstack-mounted-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde", "tokio", ] @@ -6195,7 +6672,7 @@ dependencies = [ name = "dioxus-playwright-fullstack-routing-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde", "tokio", ] @@ -6204,14 +6681,14 @@ dependencies = [ name = "dioxus-playwright-fullstack-spread-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "dioxus-playwright-fullstack-test" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "futures", "serde", "tokio", @@ -6222,7 +6699,7 @@ name = "dioxus-playwright-liveview-test" version = "0.0.1" dependencies = [ "axum 0.8.6", - "dioxus", + "dioxus 0.7.1", "dioxus-liveview", "tokio", ] @@ -6231,21 +6708,21 @@ dependencies = [ name = "dioxus-playwright-web-hash-routing-test" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "dioxus-playwright-web-routing-test" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "dioxus-playwright-web-test" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde_json", "tracing", "tracing-wasm", @@ -6257,7 +6734,7 @@ dependencies = [ name = "dioxus-pwa-example" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] @@ -6268,17 +6745,17 @@ dependencies = [ "base64 0.22.1", "ciborium", "criterion", - "dioxus", - "dioxus-cli-config", - "dioxus-core", - "dioxus-core-macro", - "dioxus-fullstack-core", - "dioxus-history", - "dioxus-hooks", - "dioxus-html", + "dioxus 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-core-macro 0.7.1", + "dioxus-fullstack-core 0.7.1", + "dioxus-history 0.7.1", + "dioxus-hooks 0.7.1", + "dioxus-html 0.7.1", "dioxus-router", "dioxus-router-macro", - "dioxus-signals", + "dioxus-signals 0.7.1", "dioxus-ssr", "percent-encoding", "rustversion", @@ -6294,7 +6771,7 @@ version = "0.7.1" dependencies = [ "base16", "digest", - "dioxus", + "dioxus 0.7.1", "proc-macro2", "quote", "sha2", @@ -6314,13 +6791,25 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "dioxus-rsx" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f026380dfda8b93ad995c0a90a62a17b8afeb246baff1b781a52c7b1b3ebd791" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.108", +] + [[package]] name = "dioxus-rsx-hotreload" version = "0.7.1" dependencies = [ - "dioxus-core", - "dioxus-core-types", - "dioxus-rsx", + "dioxus-core 0.7.1", + "dioxus-core-types 0.7.1", + "dioxus-rsx 0.7.1", "internment", "proc-macro2", "proc-macro2-diagnostics", @@ -6335,8 +6824,8 @@ version = "0.7.1" dependencies = [ "convert_case 0.8.0", "dioxus-autofmt", - "dioxus-html", - "dioxus-rsx", + "dioxus-html 0.7.1", + "dioxus-rsx 0.7.1", "html_parser", "htmlentity", "pretty_assertions", @@ -6357,26 +6846,26 @@ dependencies = [ "chrono", "ciborium", "dashmap 6.1.0", - "dioxus", - "dioxus-cli-config", - "dioxus-core", - "dioxus-core-macro", - "dioxus-devtools", - "dioxus-document", - "dioxus-fullstack-core", - "dioxus-history", - "dioxus-hooks", - "dioxus-html", - "dioxus-interpreter-js", - "dioxus-logger", + "dioxus 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-core-macro 0.7.1", + "dioxus-devtools 0.7.1", + "dioxus-document 0.7.1", + "dioxus-fullstack-core 0.7.1", + "dioxus-history 0.7.1", + "dioxus-hooks 0.7.1", + "dioxus-html 0.7.1", + "dioxus-interpreter-js 0.7.1", + "dioxus-logger 0.7.1", "dioxus-router", - "dioxus-signals", + "dioxus-signals 0.7.1", "dioxus-ssr", "enumset", "futures", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "http 1.3.1", "http-body-util", "hyper 1.7.0", @@ -6393,7 +6882,7 @@ dependencies = [ "serde", "serde_json", "serde_qs", - "subsecond", + "subsecond 0.7.1", "thiserror 2.0.17", "tokio", "tokio-tungstenite 0.27.0", @@ -6411,11 +6900,11 @@ dependencies = [ name = "dioxus-signals" version = "0.7.1" dependencies = [ - "dioxus", - "dioxus-core", + "dioxus 0.7.1", + "dioxus-core 0.7.1", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "parking_lot", "rand 0.9.2", "reqwest 0.12.24", @@ -6427,14 +6916,30 @@ dependencies = [ "warnings", ] +[[package]] +name = "dioxus-signals" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3895cc17ff5b43ada07743111be586e7a927ed7ec511457020e4235e13e63fe6" +dependencies = [ + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot", + "rustc-hash 2.1.1", + "tracing", + "warnings", +] + [[package]] name = "dioxus-ssr" version = "0.7.1" dependencies = [ "askama_escape", - "dioxus", - "dioxus-core", - "dioxus-core-types", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "dioxus-core-types 0.7.1", "rustc-hash 2.1.1", ] @@ -6442,19 +6947,42 @@ dependencies = [ name = "dioxus-stores" version = "0.7.1" dependencies = [ - "dioxus", - "dioxus-core", - "dioxus-signals", - "dioxus-stores-macro", + "dioxus 0.7.1", + "dioxus-core 0.7.1", + "dioxus-signals 0.7.1", + "dioxus-stores-macro 0.7.1", +] + +[[package]] +name = "dioxus-stores" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8521729ac35f362476ac4eb7d1c4ab79e7e92a0facfdea3ee978c0ddf7108d37" +dependencies = [ + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-stores-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dioxus-stores-macro" +version = "0.7.1" +dependencies = [ + "convert_case 0.8.0", + "dioxus 0.7.1", + "dioxus-stores 0.7.1", + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] name = "dioxus-stores-macro" version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a733d2684dc843e81954f6176b3353e4cfc71b6978a8e464591bb5536f610b" dependencies = [ "convert_case 0.8.0", - "dioxus", - "dioxus-stores", "proc-macro2", "quote", "syn 2.0.108", @@ -6464,8 +6992,8 @@ dependencies = [ name = "dioxus-tailwind" version = "0.0.0" dependencies = [ - "dioxus", - "manganis", + "dioxus 0.7.1", + "manganis 0.7.1", ] [[package]] @@ -6473,26 +7001,26 @@ name = "dioxus-web" version = "0.7.1" dependencies = [ "ciborium", - "dioxus", - "dioxus-cli-config", - "dioxus-core", - "dioxus-core-types", - "dioxus-devtools", - "dioxus-document", - "dioxus-fullstack-core", - "dioxus-history", - "dioxus-html", - "dioxus-interpreter-js", - "dioxus-signals", + "dioxus 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core 0.7.1", + "dioxus-core-types 0.7.1", + "dioxus-devtools 0.7.1", + "dioxus-document 0.7.1", + "dioxus-fullstack-core 0.7.1", + "dioxus-history 0.7.1", + "dioxus-html 0.7.1", + "dioxus-interpreter-js 0.7.1", + "dioxus-signals 0.7.1", "dioxus-ssr", - "dioxus-web", + "dioxus-web 0.7.1", "futures-channel", "futures-util", - "generational-box", + "generational-box 0.7.1", "gloo-dialogs", "gloo-timers", "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.1", "rustc-hash 2.1.1", "send_wrapper", "serde", @@ -6507,6 +7035,39 @@ dependencies = [ "web-sys", ] +[[package]] +name = "dioxus-web" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76155ecd44535e7c096ec8c5aac4a945899e47567ead4869babdaa74f3f9bca0" +dependencies = [ + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-devtools 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-document 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-history 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-html 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-interpreter-js 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-signals 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-util", + "generational-box 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "gloo-timers", + "js-sys", + "lazy-js-bundle 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-hash 2.1.1", + "send_wrapper", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "dircpy" version = "0.3.19" @@ -6709,6 +7270,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dx-macro-helpers" +version = "0.7.1" +dependencies = [ + "const-serialize 0.8.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -6747,7 +7318,7 @@ name = "ecommerce-site" version = "0.1.1" dependencies = [ "chrono", - "dioxus", + "dioxus 0.7.1", "reqwest 0.12.24", "serde", ] @@ -7261,7 +7832,7 @@ dependencies = [ name = "file-explorer" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "open", ] @@ -7546,8 +8117,8 @@ dependencies = [ "axum_session", "axum_session_auth", "axum_session_sqlx", - "dioxus", - "dioxus-web", + "dioxus 0.7.1", + "dioxus-web 0.7.1", "execute", "http 1.3.1", "serde", @@ -7561,7 +8132,7 @@ dependencies = [ name = "fullstack-desktop-example" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde", ] @@ -7570,7 +8141,7 @@ name = "fullstack-hackernews-example" version = "0.1.0" dependencies = [ "chrono", - "dioxus", + "dioxus 0.7.1", "reqwest 0.12.24", "serde", ] @@ -7580,7 +8151,7 @@ name = "fullstack-hello-world-example" version = "0.1.0" dependencies = [ "anyhow", - "dioxus", + "dioxus 0.7.1", "reqwest 0.12.24", "serde", "serde_json", @@ -7592,7 +8163,7 @@ name = "fullstack-router-example" version = "0.1.0" dependencies = [ "axum 0.8.6", - "dioxus", + "dioxus 0.7.1", "serde", "tokio", ] @@ -7830,6 +8401,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "generational-box" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c1ae09dfd2d455484a54b56129b9821241c4b0e412227806b6c3730cd18a29" +dependencies = [ + "parking_lot", + "tracing", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -7851,6 +8432,14 @@ dependencies = [ "typenum", ] +[[package]] +name = "geolocation" +version = "0.1.0" +dependencies = [ + "dioxus 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-geolocation", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -8797,14 +9386,14 @@ dependencies = [ name = "harness-default-to-non-default" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-fullstack-desktop" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] @@ -8812,7 +9401,7 @@ name = "harness-fullstack-desktop-with-default" version = "0.0.1" dependencies = [ "anyhow", - "dioxus", + "dioxus 0.7.1", ] [[package]] @@ -8820,28 +9409,28 @@ name = "harness-fullstack-desktop-with-features" version = "0.0.1" dependencies = [ "anyhow", - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-fullstack-multi-target" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-fullstack-multi-target-no-default" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-fullstack-with-optional-tokio" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde", "tokio", ] @@ -8857,14 +9446,14 @@ dependencies = [ name = "harness-renderer-swap" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-simple-dedicated-client" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] @@ -8875,42 +9464,42 @@ version = "0.0.1" name = "harness-simple-desktop" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-simple-fullstack" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-simple-fullstack-native-with-default" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-simple-fullstack-with-default" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-simple-mobile" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] name = "harness-simple-web" version = "0.0.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] @@ -9152,7 +9741,7 @@ name = "hotdog" version = "0.1.0" dependencies = [ "anyhow", - "dioxus", + "dioxus 0.7.1", "reqwest 0.12.24", "rusqlite", "serde", @@ -10425,6 +11014,12 @@ dependencies = [ name = "lazy-js-bundle" version = "0.7.1" +[[package]] +name = "lazy-js-bundle" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409273b42d0e3ae7c8ce6b8cfbc6a27b7c7d83bbb94fc7f93f22cc9b90eea078" + [[package]] name = "lazy_static" version = "1.5.0" @@ -10921,20 +11516,44 @@ dependencies = [ name = "manganis" version = "0.7.1" dependencies = [ - "const-serialize", - "manganis-core", - "manganis-macro", + "const-serialize 0.8.0", + "dx-macro-helpers", + "manganis-core 0.7.1", + "manganis-macro 0.7.1", +] + +[[package]] +name = "manganis" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "124f8f094eb75783b38209ce4d534b9617da4efac652802d9bafe05043a3ec95" +dependencies = [ + "const-serialize 0.7.1", + "manganis-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "manganis-macro 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "manganis-core" +version = "0.7.1" +dependencies = [ + "const-serialize 0.8.0", + "dioxus 0.7.1", + "dioxus-cli-config 0.7.1", + "dioxus-core-types 0.7.1", + "manganis 0.7.1", + "serde", ] [[package]] name = "manganis-core" version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fbd1fb8c5aabcc54c6b02dbc968e1c89c28f3e543f2789ef9e3ce45dbdf5df" dependencies = [ - "const-serialize", - "dioxus", - "dioxus-cli-config", - "dioxus-core-types", - "manganis", + "const-serialize 0.7.1", + "dioxus-cli-config 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dioxus-core-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", ] @@ -10943,9 +11562,24 @@ name = "manganis-macro" version = "0.7.1" dependencies = [ "dunce", + "dx-macro-helpers", "macro-string", - "manganis", - "manganis-core", + "manganis 0.7.1", + "manganis-core 0.7.1", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "manganis-macro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d6fec2a8249739bb30b53a08ecbb217f76096c08f1053f38ec3981ba424c11" +dependencies = [ + "dunce", + "macro-string", + "manganis-core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2", "quote", "syn 2.0.108", @@ -11396,7 +12030,7 @@ dependencies = [ "blitz-paint", "blitz-traits", "bytemuck", - "dioxus", + "dioxus 0.7.1", "dioxus-native-dom", "futures-util", "pollster 0.4.0", @@ -11420,9 +12054,9 @@ dependencies = [ "bytes", "crossbeam-channel", "data-url 0.3.2", - "dioxus", - "dioxus-asset-resolver", - "dioxus-devtools", + "dioxus 0.7.1", + "dioxus-asset-resolver 0.7.1", + "dioxus-devtools 0.7.1", "dioxus-native-dom", "paste", "rustc-hash 1.1.0", @@ -11530,7 +12164,7 @@ dependencies = [ name = "nested-suspense" version = "0.1.0" dependencies = [ - "dioxus", + "dioxus 0.7.1", "serde", "tokio", ] @@ -12809,6 +13443,38 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "permissions" +version = "0.7.1" +dependencies = [ + "const-serialize 0.8.0", + "dx-macro-helpers", + "permissions-core", + "permissions-macro", +] + +[[package]] +name = "permissions-core" +version = "0.7.1" +dependencies = [ + "const-serialize 0.8.0", + "const-serialize-macro 0.8.0", + "manganis-core 0.7.1", + "serde", +] + +[[package]] +name = "permissions-macro" +version = "0.7.1" +dependencies = [ + "const-serialize 0.8.0", + "dx-macro-helpers", + "permissions-core", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "pest" version = "2.8.3" @@ -13193,6 +13859,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "platform-bridge-macro" +version = "0.7.1" +dependencies = [ + "dx-macro-helpers", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "plist" version = "1.8.0" @@ -16079,7 +16755,7 @@ dependencies = [ name = "ssr-only" version = "0.7.1" dependencies = [ - "dioxus", + "dioxus 0.7.1", ] [[package]] @@ -16404,7 +17080,26 @@ dependencies = [ "memfd", "memmap2", "serde", - "subsecond-types", + "subsecond-types 0.7.1", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "834e8caec50249083ee6972a2f7645c4baadcb39d49ea801da1dc1d5e1c2ccb9" +dependencies = [ + "js-sys", + "libc", + "libloading 0.8.9", + "memfd", + "memmap2", + "serde", + "subsecond-types 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 2.0.17", "wasm-bindgen", "wasm-bindgen-futures", @@ -16417,7 +17112,7 @@ version = "0.1.0" dependencies = [ "cross-tls-crate", "cross-tls-crate-dylib", - "dioxus-devtools", + "dioxus-devtools 0.7.1", ] [[package]] @@ -16427,6 +17122,15 @@ dependencies = [ "serde", ] +[[package]] +name = "subsecond-types" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6beffea67e72a7a530990b270fd0277971eae564fdc10c1e0080e928b477fab" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -16457,7 +17161,7 @@ name = "suspense-carousel" version = "0.7.1" dependencies = [ "async-std", - "dioxus", + "dioxus 0.7.1", "serde", ] @@ -18962,7 +19666,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", - "dioxus", + "dioxus 0.7.1", "dioxus-router", "futures", "getrandom 0.3.4", @@ -19565,7 +20269,7 @@ version = "0.0.0" dependencies = [ "bytemuck", "color", - "dioxus", + "dioxus 0.7.1", "dioxus-native", "tracing-subscriber", "wgpu 26.0.1", diff --git a/Cargo.toml b/Cargo.toml index f5f8d42586..9dbf374f49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,10 @@ members = [ "packages/rsx-hotreload", "packages/const-serialize", "packages/const-serialize-macro", + "packages/dx-macro-helpers", + "packages/permissions/permissions-core", + "packages/permissions/permissions-macro", + "packages/permissions/permissions", "packages/dx-wire-format", "packages/logger", "packages/config-macros", @@ -86,6 +90,10 @@ members = [ "packages/manganis/manganis-core", "packages/manganis/manganis-macro", + # platform-bridge + "packages/platform-bridge", + "packages/platform-bridge-macro", + # wasm-split "packages/wasm-split/wasm-split", "packages/wasm-split/wasm-split-macro", @@ -105,6 +113,7 @@ members = [ "examples/01-app-demos/bluetooth-scanner", "examples/01-app-demos/file-explorer", "examples/01-app-demos/hotdog", + "examples/01-app-demos/geolocation", # Fullstack examples "examples/07-fullstack/hello-world", @@ -138,7 +147,8 @@ members = [ "packages/playwright-tests/cli-optimization", "packages/playwright-tests/wasm-split-harness", "packages/playwright-tests/default-features-disabled", - "packages/playwright-tests/fullstack-error-codes", + "packages/playwright-tests/fullstack-error-codes", "packages/geolocation", + "packages/mobile-plugin-build", ] [workspace.package] @@ -193,9 +203,29 @@ dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.1" } dioxus-cli-telemetry = { path = "packages/cli-telemetry", version = "0.7.1" } dioxus-cli-config = { path = "packages/cli-config", version = "0.7.1" } -# const-serializea -const-serialize = { path = "packages/const-serialize", version = "0.7.1" } -const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.7.1" } + +dx-macro-helpers = { path = "packages/dx-macro-helpers", version = "0.7.0" } + +# permissions +permissions-core = { path = "packages/permissions/permissions-core", version = "=0.7.1" } +permissions-macro = { path = "packages/permissions/permissions-macro", version = "=0.7.1" } +permissions = { path = "packages/permissions/permissions", version = "=0.7.1" } + +# platform bridge +dioxus-platform-bridge = { path = "packages/platform-bridge", version = "=0.7.1" } +platform-bridge-macro = { path = "packages/platform-bridge-macro", version = "=0.7.1" } + +# geolocation +dioxus-geolocation = { path = "packages/geolocation", version = "=0.7.1" } + +# mobile plugin tooling +dioxus-mobile-plugin-build = { path = "packages/mobile-plugin-build", version = "=0.7.1" } +# const-serialize +const-serialize = { path = "packages/const-serialize", version = "0.8.0" } +const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.8.0" } + +# The version of const-serialize published with 0.7.0 and 0.7.1 that the CLI should still support +const-serialize-07 = { package = "const-serialize", version = "0.7.1" } # subsecond subsecond-types = { path = "packages/subsecond/subsecond-types", version = "0.7.1" } @@ -206,6 +236,9 @@ manganis = { path = "packages/manganis/manganis", version = "0.7.1" } manganis-core = { path = "packages/manganis/manganis-core", version = "0.7.1" } manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.7.1" } +# The version of assets published with 0.7.0 and 0.7.1 that the CLI should still support +manganis-core-07 = { package = "manganis-core", version = "0.7.1" } + # wasm-split wasm-splitter = { path = "packages/wasm-split/wasm-split", version = "0.7.1" } wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.7.1" } diff --git a/examples/01-app-demos/geolocation/AGENTS.md b/examples/01-app-demos/geolocation/AGENTS.md new file mode 100644 index 0000000000..0f3190b6ea --- /dev/null +++ b/examples/01-app-demos/geolocation/AGENTS.md @@ -0,0 +1,265 @@ +You are an expert [0.7 Dioxus](https://dioxuslabs.com/learn/0.7) assistant. Dioxus 0.7 changes every api in dioxus. Only use this up to date documentation. `cx`, `Scope`, and `use_state` are gone + +Provide concise code examples with detailed descriptions + +# Dioxus Dependency + +You can add Dioxus to your `Cargo.toml` like this: + +```toml +[dependencies] +dioxus = { version = "0.7.1" } + +[features] +default = ["web", "webview", "server"] +web = ["dioxus/web"] +webview = ["dioxus/desktop"] +server = ["dioxus/server"] +``` + +# Launching your application + +You need to create a main function that sets up the Dioxus runtime and mounts your root component. + +```rust +use dioxus::prelude::*; + +fn main() { + dioxus::launch(App); +} + +#[component] +fn App() -> Element { + rsx! { "Hello, Dioxus!" } +} +``` + +Then serve with `dx serve`: + +```sh +curl -sSL http://dioxus.dev/install.sh | sh +dx serve +``` + +# UI with RSX + +```rust +rsx! { + div { + class: "container", // Attribute + color: "red", // Inline styles + width: if condition { "100%" }, // Conditional attributes + "Hello, Dioxus!" + } + // Prefer loops over iterators + for i in 0..5 { + div { "{i}" } // use elements or components directly in loops + } + if condition { + div { "Condition is true!" } // use elements or components directly in conditionals + } + + {children} // Expressions are wrapped in brace + {(0..5).map(|i| rsx! { span { "Item {i}" } })} // Iterators must be wrapped in braces +} +``` + +# Assets + +The asset macro can be used to link to local files to use in your project. All links start with `/` and are relative to the root of your project. + +```rust +rsx! { + img { + src: asset!("/assets/image.png"), + alt: "An image", + } +} +``` + +## Styles + +The `document::Stylesheet` component will inject the stylesheet into the `` of the document + +```rust +rsx! { + document::Stylesheet { + href: asset!("/assets/styles.css"), + } +} +``` + +# Components + +Components are the building blocks of apps + +* Component are functions annotated with the `#[component]` macro. +* The function name must start with a capital letter or contain an underscore. +* A component re-renders only under two conditions: + 1. Its props change (as determined by `PartialEq`). + 2. An internal reactive state it depends on is updated. + +```rust +#[component] +fn Input(mut value: Signal) -> Element { + rsx! { + input { + value, + oninput: move |e| { + *value.write() = e.value(); + }, + onkeydown: move |e| { + if e.key() == Key::Enter { + value.write().clear(); + } + }, + } + } +} +``` + +Each component accepts function arguments (props) + +* Props must be owned values, not references. Use `String` and `Vec` instead of `&str` or `&[T]`. +* Props must implement `PartialEq` and `Clone`. +* To make props reactive and copy, you can wrap the type in `ReadOnlySignal`. Any reactive state like memos and resources that read `ReadOnlySignal` props will automatically re-run when the prop changes. + +# State + +A signal is a wrapper around a value that automatically tracks where it's read and written. Changing a signal's value causes code that relies on the signal to rerun. + +## Local State + +The `use_signal` hook creates state that is local to a single component. You can call the signal like a function (e.g. `my_signal()`) to clone the value, or use `.read()` to get a reference. `.write()` gets a mutable reference to the value. + +Use `use_memo` to create a memoized value that recalculates when its dependencies change. Memos are useful for expensive calculations that you don't want to repeat unnecessarily. + +```rust +#[component] +fn Counter() -> Element { + let mut count = use_signal(|| 0); + let mut doubled = use_memo(move || count() * 2); // doubled will re-run when count changes because it reads the signal + + rsx! { + h1 { "Count: {count}" } // Counter will re-render when count changes because it reads the signal + h2 { "Doubled: {doubled}" } + button { + onclick: move |_| *count.write() += 1, // Writing to the signal rerenders Counter + "Increment" + } + button { + onclick: move |_| count.with_mut(|count| *count += 1), // use with_mut to mutate the signal + "Increment with with_mut" + } + } +} +``` + +## Context API + +The Context API allows you to share state down the component tree. A parent provides the state using `use_context_provider`, and any child can access it with `use_context` + +```rust +#[component] +fn App() -> Element { + let mut theme = use_signal(|| "light".to_string()); + use_context_provider(|| theme); // Provide a type to children + rsx! { Child {} } +} + +#[component] +fn Child() -> Element { + let theme = use_context::>(); // Consume the same type + rsx! { + div { + "Current theme: {theme}" + } + } +} +``` + +# Async + +For state that depends on an asynchronous operation (like a network request), Dioxus provides a hook called `use_resource`. This hook manages the lifecycle of the async task and provides the result to your component. + +* The `use_resource` hook takes an `async` closure. It re-runs this closure whenever any signals it depends on (reads) are updated +* The `Resource` object returned can be in several states when read: +1. `None` if the resource is still loading +2. `Some(value)` if the resource has successfully loaded + +```rust +let mut dog = use_resource(move || async move { + // api request +}); + +match dog() { + Some(dog_info) => rsx! { Dog { dog_info } }, + None => rsx! { "Loading..." }, +} +``` + +# Routing + +All possible routes are defined in a single Rust `enum` that derives `Routable`. Each variant represents a route and is annotated with `#[route("/path")]`. Dynamic Segments can capture parts of the URL path as parameters by using `:name` in the route string. These become fields in the enum variant. + +The `Router {}` component is the entry point that manages rendering the correct component for the current URL. + +You can use the `#[layout(NavBar)]` to create a layout shared between pages and place an `Outlet {}` inside your layout component. The child routes will be rendered in the outlet. + +```rust +#[derive(Routable, Clone, PartialEq)] +enum Route { + #[layout(NavBar)] // This will use NavBar as the layout for all routes + #[route("/")] + Home {}, + #[route("/blog/:id")] // Dynamic segment + BlogPost { id: i32 }, +} + +#[component] +fn NavBar() -> Element { + rsx! { + a { href: "/", "Home" } + Outlet {} // Renders Home or BlogPost + } +} + +#[component] +fn App() -> Element { + rsx! { Router:: {} } +} +``` + +```toml +dioxus = { version = "0.7.1", features = ["router"] } +``` + +# Fullstack + +Fullstack enables server rendering and ipc calls. It uses Cargo features (`server` and a client feature like `web`) to split the code into a server and client binaries. + +```toml +dioxus = { version = "0.7.1", features = ["fullstack"] } +``` + +## Server Functions + +Use the `#[post]` / `#[get]` macros to define an `async` function that will only run on the server. On the server, this macro generates an API endpoint. On the client, it generates a function that makes an HTTP request to that endpoint. + +```rust +#[post("/api/double/:path/&query")] +async fn double_server(number: i32, path: String, query: i32) -> Result { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + Ok(number * 2) +} +``` + +## Hydration + +Hydration is the process of making a server-rendered HTML page interactive on the client. The server sends the initial HTML, and then the client-side runs, attaches event listeners, and takes control of future rendering. + +### Errors +The initial UI rendered by the component on the client must be identical to the UI rendered on the server. + +* Use the `use_server_future` hook instead of `use_resource`. It runs the future on the server, serializes the result, and sends it to the client, ensuring the client has the data immediately for its first render. +* Any code that relies on browser-specific APIs (like accessing `localStorage`) must be run *after* hydration. Place this code inside a `use_effect` hook. diff --git a/examples/01-app-demos/geolocation/Cargo.toml b/examples/01-app-demos/geolocation/Cargo.toml new file mode 100644 index 0000000000..1d86d4c69e --- /dev/null +++ b/examples/01-app-demos/geolocation/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "geolocation" +version = "0.1.0" +authors = ["Sabin Regmi "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus = { version = "0.7.1", features = [] } +dioxus-geolocation = { path = "../../../packages/geolocation"} + +[features] +default = ["mobile"] +web = ["dioxus/web"] +desktop = ["dioxus/desktop"] +mobile = ["dioxus/mobile"] diff --git a/examples/01-app-demos/geolocation/Dioxus.toml b/examples/01-app-demos/geolocation/Dioxus.toml new file mode 100644 index 0000000000..866aa66ae3 --- /dev/null +++ b/examples/01-app-demos/geolocation/Dioxus.toml @@ -0,0 +1,21 @@ +[application] + +[web.app] + +# HTML title tag content +title = "geolocation" + +# include `assets` in web platform +[web.resource] + +# Additional CSS style files +style = [] + +# Additional JavaScript files +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] diff --git a/examples/01-app-demos/geolocation/README.md b/examples/01-app-demos/geolocation/README.md new file mode 100644 index 0000000000..b37798f4dd --- /dev/null +++ b/examples/01-app-demos/geolocation/README.md @@ -0,0 +1,30 @@ +# Geolocation demo + +A minimal Dioxus application that exercises the `dioxus-geolocation` plugin. The UI lets you: + +- Inspect and request location permissions using the native Android/iOS dialogs. +- Configure one-shot position requests (high-accuracy toggle + maximum cached age). +- Inspect the last reported coordinates, accuracy, altitude, heading, and speed. + +The example shares the same metadata pipeline as any plugin crate: the native Gradle/Swift +artifacts are embedded via linker symbols and bundled automatically by `dx`. + +## Running the example + +```bash +# Inside the repository root +dx serve --project examples/01-app-demos/geolocation --platform mobile +``` + +For Android/iOS you’ll need the respective toolchains installed (Android SDK/NDK, Xcode) so the +geolocation crate’s `build.rs` can build the native modules. The UI also works on desktop/web, +but location calls will return an error because the plugin only supports mobile targetsβ€”those +errors are shown inline in the demo. + +## Things to try + +1. Tap **Check permissions** to see the current OS state (granted/denied/prompt). +2. Tap **Request permissions** to trigger the native dialog from within the app. +3. Toggle *High accuracy* and set a *Max cached age* before requesting the current position. +4. Observe the coordinate grid update whenever a new reading arrives, or the error banner if the + operation fails (e.g., permissions denied or running on an unsupported platform). diff --git a/examples/01-app-demos/geolocation/assets/favicon.ico b/examples/01-app-demos/geolocation/assets/favicon.ico new file mode 100644 index 0000000000..eed0c09735 Binary files /dev/null and b/examples/01-app-demos/geolocation/assets/favicon.ico differ diff --git a/examples/01-app-demos/geolocation/assets/header.svg b/examples/01-app-demos/geolocation/assets/header.svg new file mode 100644 index 0000000000..59c96f2f2e --- /dev/null +++ b/examples/01-app-demos/geolocation/assets/header.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/examples/01-app-demos/geolocation/assets/main.css b/examples/01-app-demos/geolocation/assets/main.css new file mode 100644 index 0000000000..7783a65830 --- /dev/null +++ b/examples/01-app-demos/geolocation/assets/main.css @@ -0,0 +1,222 @@ +body { + background-color: #05060a; + color: #f4f4f5; + font-family: 'Inter', 'Segoe UI', sans-serif; + margin: 0; + min-height: 100vh; + display: flex; + justify-content: center; + padding: calc(16px + env(safe-area-inset-top, 0px)) 0 40px; +} + +.app { + width: min(960px, 100%); + padding: 0 20px; + box-sizing: border-box; +} + +.hero { + display: flex; + gap: 24px; + align-items: center; + margin-bottom: 32px; + flex-wrap: wrap; +} + +.hero img { + width: 200px; + max-width: 35%; + border-radius: 16px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); +} + +.hero__copy h1 { + margin: 0 0 8px; + font-size: clamp(28px, 6vw, 36px); +} + +.hero__copy p { + margin: 0; + line-height: 1.5; + color: #c8cad7; +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 24px; + margin-bottom: 16px; +} + +.card { + background: linear-gradient(165deg, rgba(17, 20, 32, 0.95), rgba(6, 7, 16, 0.98)); + border: 1px solid #222534; + border-radius: 16px; + padding: 24px; + box-shadow: 0 25px 45px rgba(0, 0, 0, 0.4); +} + +.card h2 { + margin-top: 0; + font-size: 1.5rem; +} + +.muted { + color: #a5a7b6; + font-size: 0.95rem; +} + +.button-row { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 16px; +} + +button { + border: none; + border-radius: 999px; + padding: 10px 18px; + font-size: 0.95rem; + cursor: pointer; + transition: background 0.2s ease; +} + +button.primary, +button { + background: linear-gradient(135deg, #8f63ff, #4d8dff); + color: white; + box-shadow: 0 10px 25px rgba(77, 141, 255, 0.25); +} + +button.secondary { + background: transparent; + color: #b3b7cf; + border: 1px solid #2f3244; +} + +button.full-width { + width: 100%; + margin-top: 16px; +} + +button.toggle { + width: fit-content; + background: #1a1d29; + border: 1px solid #2c2f40; + color: #d8d9e5; +} + +button.toggle--active { + background: #23304d; + border-color: #4b6cff; + color: #ffffff; +} + +.settings { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.field input { + background: #0b0d13; + border: 1px solid #26293a; + border-radius: 10px; + padding: 10px 12px; + color: white; +} + +.status-grid { + margin-top: 20px; + display: grid; + gap: 14px; +} + +.permission-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.badge { + padding: 4px 10px; + border-radius: 999px; + font-size: 0.85rem; + text-transform: uppercase; +} + +.badge--granted { + background: rgba(70, 221, 154, 0.15); + color: #7efac6; + border: 1px solid rgba(70, 221, 154, 0.4); +} + +.badge--denied { + background: rgba(255, 98, 98, 0.16); + color: #ff8ea0; + border: 1px solid rgba(255, 98, 98, 0.4); +} + +.badge--prompt { + background: rgba(255, 205, 112, 0.16); + color: #ffd27e; + border: 1px solid rgba(255, 205, 112, 0.35); +} + +.position { + margin-top: 20px; +} + +.position__grid { + margin-top: 14px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px; +} + +.coordinate-row { + display: flex; + flex-direction: column; + gap: 2px; + padding: 10px; + background: #080a11; + border-radius: 12px; + border: 1px solid #1c1f2b; +} + +.error-banner { + margin-top: 24px; + padding: 14px 18px; + background: #3c1017; + border: 1px solid #a44856; + border-radius: 12px; + color: #ffe6ea; +} + +@media (max-width: 640px) { + .hero { + flex-direction: column; + text-align: center; + } + + .hero img { + max-width: 60%; + } + + .button-row { + flex-direction: column; + } + + button { + width: 100%; + text-align: center; + } +} diff --git a/examples/01-app-demos/geolocation/src/main.rs b/examples/01-app-demos/geolocation/src/main.rs new file mode 100644 index 0000000000..1d82660f91 --- /dev/null +++ b/examples/01-app-demos/geolocation/src/main.rs @@ -0,0 +1,226 @@ +use dioxus::prelude::*; +use dioxus_geolocation::{ + Geolocation, PermissionState, PermissionStatus, Position, PositionOptions, +}; + +const FAVICON: Asset = asset!("/assets/favicon.ico"); +const MAIN_CSS: Asset = asset!("/assets/main.css"); +const HEADER_SVG: Asset = asset!("/assets/header.svg"); + +fn main() { + dioxus::launch(App); +} + +#[component] +fn App() -> Element { + let geolocation = use_signal(Geolocation::new); + let permission_status = use_signal(|| None::); + let last_position = use_signal(|| None::); + let error = use_signal(|| None::); + let use_high_accuracy = use_signal(|| true); + let max_age_input = use_signal(|| String::from("0")); + + let on_check_permissions = { + let mut geolocation = geolocation.clone(); + let mut permission_status = permission_status.clone(); + let mut error = error.clone(); + move |_| match geolocation.write().check_permissions() { + Ok(status) => { + permission_status.set(Some(status)); + error.set(None); + } + Err(err) => error.set(Some(err.to_string())), + } + }; + + let on_request_permissions = { + let mut geolocation = geolocation.clone(); + let mut permission_status = permission_status.clone(); + let mut error = error.clone(); + move |_| { + let mut geo = geolocation.write(); + match geo.request_permissions(None) { + Ok(_) => match geo.check_permissions() { + Ok(status) => { + permission_status.set(Some(status)); + error.set(None); + } + Err(err) => error.set(Some(err.to_string())), + }, + Err(err) => error.set(Some(err.to_string())), + } + } + }; + + let on_toggle_accuracy = { + let mut use_high_accuracy = use_high_accuracy.clone(); + move |_| { + let next = !use_high_accuracy(); + use_high_accuracy.set(next); + } + }; + + let on_max_age_input = { + let mut max_age_input = max_age_input.clone(); + move |evt: FormEvent| max_age_input.set(evt.value()) + }; + + let on_fetch_position = { + let mut geolocation = geolocation.clone(); + let mut last_position = last_position.clone(); + let mut error = error.clone(); + let use_high_accuracy = use_high_accuracy.clone(); + let max_age_input = max_age_input.clone(); + move |_| { + let maximum_age = max_age_input.read().trim().parse::().unwrap_or(0); + + let options = PositionOptions { + enable_high_accuracy: use_high_accuracy(), + timeout: 10_000, + maximum_age, + }; + + match geolocation.write().get_current_position(Some(options)) { + Ok(position) => { + last_position.set(Some(position)); + error.set(None); + } + Err(err) => error.set(Some(err.to_string())), + } + } + }; + + let accuracy_label = if use_high_accuracy() { + "High accuracy: on" + } else { + "High accuracy: off" + }; + + rsx! { + document::Link { rel: "icon", href: FAVICON } + document::Link { rel: "stylesheet", href: MAIN_CSS } + + main { class: "app", + header { class: "hero", + img { src: HEADER_SVG, alt: "Map illustration" } + div { class: "hero__copy", + h1 { "Geolocation plugin demo" } + p { "One-shot location fetching through the Dioxus geolocation plugin. + Measure permissions, request access, and inspect the last fix received from the device." } + } + } + + div { class: "cards", + section { class: "card", + h2 { "Permissions" } + p { class: "muted", + "First, inspect what the OS currently allows this app to do. \ + On Android & iOS these calls talk to the native permission dialog APIs." } + div { class: "button-row", + button { onclick: on_check_permissions, "Check permissions" } + button { class: "secondary", onclick: on_request_permissions, "Request permissions" } + } + match permission_status() { + Some(status) => rsx! { + div { class: "status-grid", + PermissionBadge { label: "Location".to_string(), state: status.location } + PermissionBadge { label: "Coarse location".to_string(), state: status.coarse_location } + } + }, + None => rsx!(p { class: "muted", "Tap β€œCheck permissions” to see the current status." }), + } + } + + section { class: "card", + h2 { "Current position" } + p { class: "muted", + "The plugin resolves the device location once per request (no background watch). \ + Configure the query and then fetch the coordinates." } + div { class: "settings", + button { + class: if use_high_accuracy() { "toggle toggle--active" } else { "toggle" }, + onclick: on_toggle_accuracy, + "{accuracy_label}" + } + label { class: "field", + span { "Max cached age (ms)" } + input { + r#type: "number", + inputmode: "numeric", + min: "0", + placeholder: "0", + value: "{max_age_input()}", + oninput: on_max_age_input, + } + } + } + button { class: "primary full-width", onclick: on_fetch_position, "Get current position" } + + match last_position() { + Some(position) => { + let snapshot = position.clone(); + let coords = snapshot.coords.clone(); + rsx! { + div { class: "position", + h3 { "Latest reading" } + p { class: "muted", "Timestamp: {snapshot.timestamp} ms since Unix epoch" } + div { class: "position__grid", + CoordinateRow { label: "Latitude".to_string(), value: format!("{:.6}", coords.latitude) } + CoordinateRow { label: "Longitude".to_string(), value: format!("{:.6}", coords.longitude) } + CoordinateRow { label: "Accuracy (m)".to_string(), value: format!("{:.1}", coords.accuracy) } + CoordinateRow { label: "Altitude (m)".to_string(), value: format_optional(coords.altitude) } + CoordinateRow { label: "Altitude accuracy (m)".to_string(), value: format_optional(coords.altitude_accuracy) } + CoordinateRow { label: "Speed (m/s)".to_string(), value: format_optional(coords.speed) } + CoordinateRow { label: "Heading (Β°)".to_string(), value: format_optional(coords.heading) } + } + } + } + } + None => rsx!(p { class: "muted", "No location fetched yet." }), + } + } + } + + if let Some(message) = error() { + div { class: "error-banner", "Last error: {message}" } + } + } + } +} + +#[component] +fn PermissionBadge(label: String, state: PermissionState) -> Element { + let (text, class) = permission_state_badge(state); + rsx! { + div { class: "permission-row", + span { class: "muted", "{label}" } + span { class: class, "{text}" } + } + } +} + +#[component] +fn CoordinateRow(label: String, value: String) -> Element { + rsx! { + div { class: "coordinate-row", + span { class: "muted", "{label}" } + strong { "{value}" } + } + } +} + +fn permission_state_badge(state: PermissionState) -> (&'static str, &'static str) { + match state { + PermissionState::Granted => ("Granted", "badge badge--granted"), + PermissionState::Denied => ("Denied", "badge badge--denied"), + PermissionState::Prompt | PermissionState::PromptWithRationale => { + ("Needs prompt", "badge badge--prompt") + } + } +} + +fn format_optional(value: Option) -> String { + value + .map(|inner| format!("{inner:.2}")) + .unwrap_or_else(|| "β€”".to_string()) +} diff --git a/packages/cli-opt/Cargo.toml b/packages/cli-opt/Cargo.toml index d9a93a88ba..b3139a0cc7 100644 --- a/packages/cli-opt/Cargo.toml +++ b/packages/cli-opt/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["dom", "ui", "gui", "react"] anyhow = { workspace = true } manganis = { workspace = true } manganis-core = { workspace = true } +permissions = { workspace = true } object = { workspace = true, features = ["wasm"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/packages/cli-opt/src/lib.rs b/packages/cli-opt/src/lib.rs index 45c8c84f8f..14b0b8565a 100644 --- a/packages/cli-opt/src/lib.rs +++ b/packages/cli-opt/src/lib.rs @@ -19,6 +19,9 @@ mod json; pub use file::process_file_to; pub use hash::add_hash_to_asset; +// Re-export SymbolData from the public permissions crate for convenience +pub use permissions::SymbolData; + /// A manifest of all assets collected from dependencies /// /// This will be filled in primarily by incremental compilation artifacts. diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index ddbe943d19..6ec8bd12b8 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -28,6 +28,8 @@ depinfo = { workspace = true } subsecond-types = { workspace = true } dioxus-cli-telemetry = { workspace = true } dioxus-component-manifest = { workspace = true } +permissions = { workspace = true } +dioxus-platform-bridge = { workspace = true, features = ["metadata"] } clap = { workspace = true, features = ["derive", "cargo"] } convert_case = { workspace = true } @@ -98,6 +100,7 @@ brotli = "8.0.1" ignore = "0.4.23" env_logger = { workspace = true } const-serialize = { workspace = true, features = ["serde"] } +const-serialize-07 = { workspace = true, features = ["serde"] } tracing-subscriber = { version = "0.3.19", features = [ "std", @@ -122,6 +125,7 @@ log = { version = "0.4", features = ["max_level_off", "release_max_level_off"] } tempfile = "3.19.1" manganis = { workspace = true } manganis-core = { workspace = true } +manganis-core-07 = { workspace = true } target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] } wasm-encoder = "0.235.0" diff --git a/packages/cli/assets/android/MainActivity.kt.hbs b/packages/cli/assets/android/MainActivity.kt.hbs index 15cc9e386f..92943dd16b 100644 --- a/packages/cli/assets/android/MainActivity.kt.hbs +++ b/packages/cli/assets/android/MainActivity.kt.hbs @@ -1,8 +1,5 @@ -package dev.dioxus.main; - -// need to re-export buildconfig down from the parent -import {{application_id}}.BuildConfig; -typealias BuildConfig = BuildConfig; +package dev.dioxus.main +typealias BuildConfig = {{application_id}}.BuildConfig class MainActivity : WryActivity() diff --git a/packages/cli/assets/android/gen/app/build.gradle.kts.hbs b/packages/cli/assets/android/gen/app/build.gradle.kts.hbs index 882e4ff85c..7aa4fe09d3 100644 --- a/packages/cli/assets/android/gen/app/build.gradle.kts.hbs +++ b/packages/cli/assets/android/gen/app/build.gradle.kts.hbs @@ -53,6 +53,11 @@ android { buildFeatures { buildConfig = true } + sourceSets { + getByName("main") { + java.srcDirs("src/main/kotlin", "src/main/java") + } + } } dependencies { diff --git a/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs b/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs index 9fdcded4b5..8e317cc37d 100644 --- a/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs +++ b/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs @@ -1,6 +1,7 @@ + UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + + diff --git a/packages/cli/src/build/android_java.rs b/packages/cli/src/build/android_java.rs new file mode 100644 index 0000000000..9afb75dd1b --- /dev/null +++ b/packages/cli/src/build/android_java.rs @@ -0,0 +1,23 @@ +//! Android artifact manifest helpers. + +use permissions::AndroidArtifactMetadata; + +/// Manifest of all Android artifacts declared by dependencies. +#[derive(Debug, Clone, Default)] +pub struct AndroidArtifactManifest { + artifacts: Vec, +} + +impl AndroidArtifactManifest { + pub fn new(artifacts: Vec) -> Self { + Self { artifacts } + } + + pub fn artifacts(&self) -> &[AndroidArtifactMetadata] { + &self.artifacts + } + + pub fn is_empty(&self) -> bool { + self.artifacts.is_empty() + } +} diff --git a/packages/cli/src/build/assets.rs b/packages/cli/src/build/assets.rs index 4ca40f3a53..2fc4aad53c 100644 --- a/packages/cli/src/build/assets.rs +++ b/packages/cli/src/build/assets.rs @@ -11,10 +11,10 @@ //! process in the build system. //! //! We use the same lessons learned from the hot-patching engine which parses the binary file and its -//! symbol table to find symbols that match the `__MANGANIS__` prefix. These symbols are ideally data +//! symbol table to find symbols that match the `__ASSETS__` prefix. These symbols are ideally data //! symbols and contain the BundledAsset data type which implements ConstSerialize and ConstDeserialize. //! -//! When the binary is built, the `dioxus asset!()` macro will emit its metadata into the __MANGANIS__ +//! When the binary is built, the `dioxus asset!()` macro will emit its metadata into the __ASSETS__ //! symbols, which we process here. After reading the metadata directly from the executable, we then //! hash it and write the hash directly into the binary file. //! @@ -23,7 +23,7 @@ //! can be found relative to the current exe. Unfortunately, on android, the `current_exe` path is wrong, //! so the assets are resolved against the "asset root" - which is covered by the asset loader crate. //! -//! Finding the __MANGANIS__ symbols is not quite straightforward when hotpatching, especially on WASM +//! Finding the __ASSETS__ symbols is not quite straightforward when hotpatching, especially on WASM //! since we build and link the module as relocatable, which is not a stable WASM proposal. In this //! implementation, we handle both the non-PIE *and* PIC cases which are rather bespoke to our whole //! build system. @@ -35,34 +35,338 @@ use std::{ use crate::Result; use anyhow::{bail, Context}; -use const_serialize::{ConstVec, SerializeConst}; +use const_serialize::{deserialize_const, serialize_const, ConstVec}; use dioxus_cli_opt::AssetManifest; -use manganis::BundledAsset; +use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize}; use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol}; use pdb::FallibleIterator; +use permissions::{AndroidArtifactMetadata, Permission, SwiftPackageMetadata, SymbolData}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; /// Extract all manganis symbols and their sections from the given object file. fn manganis_symbols<'a, 'b, R: ReadRef<'a>>( file: &'b File<'a, R>, -) -> impl Iterator, Section<'a, 'b, R>)> + 'b { - file.symbols() - .filter(|symbol| { - if let Ok(name) = symbol.name() { - looks_like_manganis_symbol(name) - } else { - false +) -> impl Iterator, Section<'a, 'b, R>)> + 'b { + file.symbols().filter_map(move |symbol| { + let name = symbol.name().ok()?; + let version = looks_like_manganis_symbol(name)?; + let section_index = symbol.section_index()?; + let section = file.section_by_index(section_index).ok()?; + Some((version, symbol, section)) + }) +} + +#[derive(Copy, Clone)] +enum ManganisVersion { + /// The legacy version of the manganis format published with 0.7.0 and 0.7.1 + Legacy, + /// The new version of the manganis format 0.7.2 onward + /// This now includes both assets (old BundledAsset format) and permissions (SymbolData format) + New, +} + +impl ManganisVersion { + fn size(&self) -> usize { + match self { + ManganisVersion::Legacy => { + ::MEMORY_LAYOUT.size() + } + // For new format, we use a larger buffer size to accommodate variable-length CBOR + // The actual size will be determined by CBOR deserialization + ManganisVersion::New => 4096, + } + } + + /// Deserialize data, trying multiple formats for backward compatibility + /// + /// Tries in order: + /// 1. SymbolData (new unified format) - can contain Asset or Permission + /// 2. BundledAsset (old asset format) - for backward compatibility + fn deserialize(&self, data: &[u8]) -> Option { + match self { + ManganisVersion::Legacy => { + let buffer = const_serialize_07::ConstReadBuffer::new(data); + + let (_, legacy_asset) = + const_serialize_07::deserialize_const!(manganis_core_07::BundledAsset, buffer)?; + + Some(SymbolDataOrAsset::Asset(legacy_asset_to_modern_asset( + &legacy_asset, + ))) + } + ManganisVersion::New => { + // First try SymbolData (new format with enum variant) + // const-serialize deserialization returns (remaining_bytes, value) + // We accept if remaining is empty or contains only padding (zeros) + if let Some((remaining, symbol_data)) = deserialize_const!(SymbolData, data) { + // Check if remaining bytes are all zeros (padding) or empty + // This handles the case where the linker section is larger than the actual data + // Be very lenient with padding - as long as we successfully deserialized, accept it + // The padding is just zeros added to fill the buffer size + let is_valid = remaining.is_empty() + || remaining.iter().all(|&b| b == 0) + || remaining.len() <= data.len(); // Allow any amount of padding as long as it's not larger than data + + if is_valid { + return Some(SymbolDataOrAsset::SymbolData(symbol_data)); + } else { + tracing::debug!( + "SymbolData deserialized but invalid padding: {} remaining bytes out of {} total (first few bytes: {:?})", + remaining.len(), + data.len(), + &data[..data.len().min(32)] + ); + } + } else { + tracing::debug!( + "Failed to deserialize as SymbolData. Data length: {}, first few bytes: {:?}", + data.len(), + &data[..data.len().min(32)] + ); + } + + // Fallback: try BundledAsset (direct format - assets are now serialized this way) + // This handles assets that were serialized directly as BundledAsset (not wrapped in SymbolData) + if let Some((remaining, asset)) = deserialize_const!(BundledAsset, data) { + // Check if remaining bytes are all zeros (padding) or empty + // Accept any amount of padding as long as it's all zeros (which is what we pad with) + let is_valid = remaining.is_empty() || remaining.iter().all(|&b| b == 0); + + if is_valid { + tracing::debug!( + "Successfully deserialized BundledAsset, remaining padding: {} bytes", + remaining.len() + ); + return Some(SymbolDataOrAsset::Asset(asset)); + } else { + tracing::warn!( + "BundledAsset deserialized but remaining bytes are not all zeros: {} remaining bytes, first few: {:?}", + remaining.len(), + &remaining[..remaining.len().min(16)] + ); + } + } else { + tracing::warn!( + "Failed to deserialize as BundledAsset. Data length: {}, first 32 bytes: {:?}", + data.len(), + &data[..data.len().min(32)] + ); + } + + None + } + } + } + + fn serialize_asset(&self, asset: &BundledAsset) -> Vec { + match self { + ManganisVersion::Legacy => { + let legacy_asset = modern_asset_to_legacy_asset(asset); + let buffer = const_serialize_07::serialize_const( + &legacy_asset, + const_serialize_07::ConstVec::new(), + ); + buffer.as_ref().to_vec() + } + ManganisVersion::New => { + // New format: serialize as BundledAsset directly (backward compatible) + // Pad to 4096 bytes to match the linker output size + let buffer = serialize_const(asset, ConstVec::new()); + let mut data = buffer.as_ref().to_vec(); + if data.len() < 4096 { + data.resize(4096, 0); + } + data } - }) - .filter_map(move |symbol| { - let section_index = symbol.section_index()?; - let section = file.section_by_index(section_index).ok()?; - Some((symbol, section)) - }) + } + } + + fn serialize_symbol_data(&self, data: &SymbolData) -> Option> { + match self { + ManganisVersion::Legacy => None, + ManganisVersion::New => { + let buffer = serialize_const(data, ConstVec::new()); + let mut bytes = buffer.as_ref().to_vec(); + if bytes.len() < 4096 { + bytes.resize(4096, 0); + } + Some(bytes) + } + } + } +} + +/// Result of deserializing a symbol - can be either SymbolData or legacy Asset +#[derive(Debug, Clone)] +enum SymbolDataOrAsset { + /// New unified format (can contain Asset or Permission) + SymbolData(SymbolData), + /// Old asset format (backward compatibility) + Asset(BundledAsset), +} + +#[derive(Clone, Copy)] +struct AssetWriteEntry { + symbol: ManganisSymbolOffset, + asset_index: usize, + representation: AssetRepresentation, +} + +impl AssetWriteEntry { + fn new( + symbol: ManganisSymbolOffset, + asset_index: usize, + representation: AssetRepresentation, + ) -> Self { + Self { + symbol, + asset_index, + representation, + } + } } -fn looks_like_manganis_symbol(name: &str) -> bool { - name.contains("__MANGANIS__") +#[derive(Clone, Copy)] +enum AssetRepresentation { + /// Serialized as a raw BundledAsset (legacy or new format) + RawBundled, + /// Serialized as SymbolData::Asset (new CBOR format) + SymbolData, +} + +fn legacy_asset_to_modern_asset( + legacy_asset: &manganis_core_07::BundledAsset, +) -> manganis_core::BundledAsset { + let bundled_path = legacy_asset.bundled_path(); + let absolute_path = legacy_asset.absolute_source_path(); + let legacy_options = legacy_asset.options(); + let add_hash = legacy_options.hash_suffix(); + let options = match legacy_options.variant() { + manganis_core_07::AssetVariant::Image(image) => { + let format = match image.format() { + manganis_core_07::ImageFormat::Png => ImageFormat::Png, + manganis_core_07::ImageFormat::Jpg => ImageFormat::Jpg, + manganis_core_07::ImageFormat::Webp => ImageFormat::Webp, + manganis_core_07::ImageFormat::Avif => ImageFormat::Avif, + manganis_core_07::ImageFormat::Unknown => ImageFormat::Unknown, + }; + let size = match image.size() { + manganis_core_07::ImageSize::Automatic => ImageSize::Automatic, + manganis_core_07::ImageSize::Manual { width, height } => { + ImageSize::Manual { width, height } + } + }; + let preload = image.preloaded(); + + AssetOptions::image() + .with_format(format) + .with_size(size) + .with_preload(preload) + .with_hash_suffix(add_hash) + .into_asset_options() + } + manganis_core_07::AssetVariant::Folder(_) => AssetOptions::folder() + .with_hash_suffix(add_hash) + .into_asset_options(), + manganis_core_07::AssetVariant::Css(css) => AssetOptions::css() + .with_hash_suffix(add_hash) + .with_minify(css.minified()) + .with_preload(css.preloaded()) + .with_static_head(css.static_head()) + .into_asset_options(), + manganis_core_07::AssetVariant::CssModule(css_module) => AssetOptions::css_module() + .with_hash_suffix(add_hash) + .with_minify(css_module.minified()) + .with_preload(css_module.preloaded()) + .into_asset_options(), + manganis_core_07::AssetVariant::Js(js) => AssetOptions::js() + .with_hash_suffix(add_hash) + .with_minify(js.minified()) + .with_preload(js.preloaded()) + .with_static_head(js.static_head()) + .into_asset_options(), + _ => AssetOptions::builder().into_asset_options(), + }; + + BundledAsset::new(absolute_path, bundled_path, options) +} + +fn modern_asset_to_legacy_asset(modern_asset: &BundledAsset) -> manganis_core_07::BundledAsset { + let bundled_path = modern_asset.bundled_path(); + let absolute_path = modern_asset.absolute_source_path(); + let legacy_options = modern_asset.options(); + let add_hash = legacy_options.hash_suffix(); + let options = match legacy_options.variant() { + AssetVariant::Image(image) => { + let format = match image.format() { + ImageFormat::Png => manganis_core_07::ImageFormat::Png, + ImageFormat::Jpg => manganis_core_07::ImageFormat::Jpg, + ImageFormat::Webp => manganis_core_07::ImageFormat::Webp, + ImageFormat::Avif => manganis_core_07::ImageFormat::Avif, + ImageFormat::Unknown => manganis_core_07::ImageFormat::Unknown, + }; + let size = match image.size() { + ImageSize::Automatic => manganis_core_07::ImageSize::Automatic, + ImageSize::Manual { width, height } => { + manganis_core_07::ImageSize::Manual { width, height } + } + }; + let preload = image.preloaded(); + + manganis_core_07::AssetOptions::image() + .with_format(format) + .with_size(size) + .with_preload(preload) + .with_hash_suffix(add_hash) + .into_asset_options() + } + AssetVariant::Folder(_) => manganis_core_07::AssetOptions::folder() + .with_hash_suffix(add_hash) + .into_asset_options(), + AssetVariant::Css(css) => manganis_core_07::AssetOptions::css() + .with_hash_suffix(add_hash) + .with_minify(css.minified()) + .with_preload(css.preloaded()) + .with_static_head(css.static_head()) + .into_asset_options(), + AssetVariant::CssModule(css_module) => manganis_core_07::AssetOptions::css_module() + .with_hash_suffix(add_hash) + .with_minify(css_module.minified()) + .with_preload(css_module.preloaded()) + .into_asset_options(), + AssetVariant::Js(js) => manganis_core_07::AssetOptions::js() + .with_hash_suffix(add_hash) + .with_minify(js.minified()) + .with_preload(js.preloaded()) + .with_static_head(js.static_head()) + .into_asset_options(), + _ => manganis_core_07::AssetOptions::builder().into_asset_options(), + }; + + manganis_core_07::BundledAsset::new(absolute_path, bundled_path, options) +} + +fn looks_like_manganis_symbol(name: &str) -> Option { + if name.contains("__MANGANIS__") { + Some(ManganisVersion::Legacy) + } else if name.contains("__ASSETS__") { + Some(ManganisVersion::New) + } else { + None + } +} + +/// An asset offset in the binary +#[derive(Clone, Copy)] +struct ManganisSymbolOffset { + version: ManganisVersion, + offset: u64, +} + +impl ManganisSymbolOffset { + fn new(version: ManganisVersion, offset: u64) -> Self { + Self { version, offset } + } } /// Find the offsets of any manganis symbols in the given file. @@ -70,7 +374,7 @@ fn find_symbol_offsets<'a, R: ReadRef<'a>>( path: &Path, file_contents: &[u8], file: &File<'a, R>, -) -> Result> { +) -> Result> { let pdb_file = find_pdb_file(path); match file.format() { @@ -118,7 +422,7 @@ fn find_pdb_file(path: &Path) -> Option { } /// Find the offsets of any manganis symbols in a pdb file. -fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result> { +fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result> { let pdb_file_handle = std::fs::File::open(pdb_file)?; let mut pdb_file = pdb::PDB::open(pdb_file_handle).context("Failed to open PDB file")?; let Ok(Some(sections)) = pdb_file.sections() else { @@ -142,26 +446,31 @@ fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result> { }; let name = data.name.to_string(); - if name.contains("__MANGANIS__") { + if let Some(version) = looks_like_manganis_symbol(&name) { let section = sections .get(rva.section as usize - 1) .expect("Section index out of bounds"); - addresses.push((section.pointer_to_raw_data + rva.offset) as u64); + addresses.push(ManganisSymbolOffset::new( + version, + (section.pointer_to_raw_data + rva.offset) as u64, + )); } } Ok(addresses) } /// Find the offsets of any manganis symbols in a native object file. -fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result> { +fn find_native_symbol_offsets<'a, R: ReadRef<'a>>( + file: &File<'a, R>, +) -> Result> { let mut offsets = Vec::new(); - for (symbol, section) in manganis_symbols(file) { + for (version, symbol, section) in manganis_symbols(file) { let virtual_address = symbol.address(); let Some((section_range_start, _)) = section.file_range() else { tracing::error!( - "Found __MANGANIS__ symbol {:?} in section {}, but the section has no file range", + "Found __ASSETS__ symbol {:?} in section {}, but the section has no file range", symbol.name(), section.index() ); @@ -172,7 +481,7 @@ fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result< .try_into() .expect("Virtual address should be greater than or equal to section address"); let file_offset = section_range_start + section_relative_address; - offsets.push(file_offset); + offsets.push(ManganisSymbolOffset::new(version, file_offset)); } Ok(offsets) @@ -198,7 +507,7 @@ fn eval_walrus_global_expr(module: &walrus::Module, expr: &walrus::ConstExpr) -> fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>( file_contents: &[u8], file: &File<'a, R>, -) -> Result> { +) -> Result> { let Some(section) = file .sections() .find(|section| section.name() == Ok("")) @@ -259,9 +568,9 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>( eval_walrus_global_expr(&module, &main_memory_offset).unwrap_or_default(); for export in module.exports.iter() { - if !looks_like_manganis_symbol(&export.name) { + let Some(version) = looks_like_manganis_symbol(&export.name) else { continue; - } + }; let walrus::ExportItem::Global(global) = export.item else { continue; @@ -273,7 +582,7 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>( let Some(virtual_address) = eval_walrus_global_expr(&module, &pointer) else { tracing::error!( - "Found __MANGANIS__ symbol {:?} in WASM file, but the global expression could not be evaluated", + "Found __ASSETS__ symbol {:?} in WASM file, but the global expression could not be evaluated", export.name ); continue; @@ -285,15 +594,30 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>( .expect("Virtual address should be greater than or equal to section address"); let file_offset = data_start_offset + section_relative_address; - offsets.push(file_offset); + offsets.push(ManganisSymbolOffset::new(version, file_offset)); } Ok(offsets) } -/// Find all assets in the given file, hash them, and write them back to the file. -/// Then return an `AssetManifest` containing all the assets found in the file. -pub(crate) async fn extract_assets_from_file(path: impl AsRef) -> Result { +/// Result of extracting symbols from a binary file +#[derive(Debug, Clone)] +pub(crate) struct SymbolExtractionResult { + /// Assets found in the binary + pub assets: Vec, + /// Permissions found in the binary + pub permissions: Vec, + /// Android plugin artifacts discovered in the binary + pub android_artifacts: Vec, + /// Swift packages discovered in the binary + pub swift_packages: Vec, +} + +/// Find all assets and permissions in the given file, hash assets, and write them back to the file. +/// Then return both assets and permissions found in the file. +pub(crate) async fn extract_symbols_from_file( + path: impl AsRef, +) -> Result { let path = path.as_ref(); let mut file = open_file_for_writing_with_timeout( path, @@ -309,24 +633,92 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef) -> Result { + match symbol_data { + SymbolData::Asset(asset) => { + tracing::debug!( + "Found asset (via SymbolData) at offset {offset}: {:?}", + asset.absolute_source_path() + ); + let asset_index = assets.len(); + assets.push(asset); + write_entries.push(AssetWriteEntry::new( + symbol, + asset_index, + AssetRepresentation::SymbolData, + )); + } + SymbolData::Permission(permission) => { + tracing::debug!( + "Found permission at offset {offset}: {:?} - {}", + permission.kind(), + permission.description() + ); + permissions.push(permission); + // Permissions are not written back, so don't store the symbol + } + SymbolData::AndroidArtifact(meta) => { + tracing::debug!( + "Found Android artifact declaration for plugin {}", + meta.plugin_name.as_str() + ); + android_artifacts.push(meta); + } + SymbolData::SwiftPackage(meta) => { + tracing::debug!( + "Found Swift package declaration for plugin {}", + meta.plugin_name.as_str() + ); + swift_packages.push(meta); + } + } + } + SymbolDataOrAsset::Asset(asset) => { + tracing::debug!( + "Found asset (old format) at offset {offset}: {:?}", + asset.absolute_source_path() + ); + let asset_index = assets.len(); + assets.push(asset); + write_entries.push(AssetWriteEntry::new( + symbol, + asset_index, + AssetRepresentation::RawBundled, + )); + } + } } else { - tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions."); + tracing::warn!("Found a symbol at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions, or the symbol may be in an unsupported format."); } } @@ -335,15 +727,47 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef) -> Result { + tracing::debug!("Writing asset to offset {offset}: {:?}", asset); + let new_data = version.serialize_asset(&asset); + if new_data.len() > version.size() { + tracing::warn!( + "Asset at offset {offset} serialized to {} bytes, but buffer is only {} bytes. Truncating output.", + new_data.len(), + version.size() + ); + } + write_serialized_bytes(&mut file, offset, &new_data, version.size())?; + } + AssetRepresentation::SymbolData => { + tracing::debug!("Writing asset (SymbolData) to offset {offset}: {:?}", asset); + let Some(new_data) = version.serialize_symbol_data(&SymbolData::Asset(asset)) + else { + tracing::warn!( + "Symbol at offset {offset} was stored as SymbolData but the binary format only supports raw assets" + ); + continue; + }; + if new_data.len() > version.size() { + tracing::warn!( + "SymbolData asset at offset {offset} serialized to {} bytes, but buffer is only {} bytes. Truncating output.", + new_data.len(), + version.size() + ); + } + write_serialized_bytes(&mut file, offset, &new_data, version.size())?; + } + } } // Ensure the file is flushed to disk @@ -369,12 +793,26 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef) -> Result) -> Result { + let result = extract_symbols_from_file(path).await?; let mut manifest = AssetManifest::default(); - for asset in assets { + for asset in result.assets { manifest.insert_asset(asset); } - Ok(manifest) } @@ -404,3 +842,25 @@ async fn open_file_for_writing_with_timeout( } } } + +fn write_serialized_bytes( + file: &mut std::fs::File, + offset: u64, + data: &[u8], + buffer_size: usize, +) -> Result<()> { + use std::io::SeekFrom; + + file.seek(SeekFrom::Start(offset))?; + if data.len() <= buffer_size { + file.write_all(data)?; + if data.len() < buffer_size { + let padding = vec![0; buffer_size - data.len()]; + file.write_all(&padding)?; + } + } else { + file.write_all(&data[..buffer_size])?; + } + + Ok(()) +} diff --git a/packages/cli/src/build/ios_swift.rs b/packages/cli/src/build/ios_swift.rs new file mode 100644 index 0000000000..053c878345 --- /dev/null +++ b/packages/cli/src/build/ios_swift.rs @@ -0,0 +1,23 @@ +//! iOS/macOS Swift package manifest helpers. + +use permissions::SwiftPackageMetadata as SwiftSourceMetadata; + +/// Manifest of Swift packages embedded in the binary. +#[derive(Debug, Clone, Default)] +pub struct SwiftSourceManifest { + sources: Vec, +} + +impl SwiftSourceManifest { + pub fn new(sources: Vec) -> Self { + Self { sources } + } + + pub fn sources(&self) -> &[SwiftSourceMetadata] { + &self.sources + } + + pub fn is_empty(&self) -> bool { + self.sources.is_empty() + } +} diff --git a/packages/cli/src/build/mod.rs b/packages/cli/src/build/mod.rs index c1cf1fcfa5..d561e38311 100644 --- a/packages/cli/src/build/mod.rs +++ b/packages/cli/src/build/mod.rs @@ -8,11 +8,14 @@ //! hot-patching Rust code through binary analysis and a custom linker. The [`builder`] module contains //! the management of the ongoing build and methods to open the build as a running app. +mod android_java; mod assets; mod builder; mod context; +mod ios_swift; mod manifest; mod patch; +mod permissions; mod pre_render; mod request; mod tools; diff --git a/packages/cli/src/build/permissions.rs b/packages/cli/src/build/permissions.rs new file mode 100644 index 0000000000..a5cb9825c8 --- /dev/null +++ b/packages/cli/src/build/permissions.rs @@ -0,0 +1,89 @@ +//! The dioxus permission system. +//! +//! This module extracts permissions from compiled binaries and generates platform-specific +//! manifest files for platforms that require build-time permission declarations. +//! +//! Platforms requiring build-time manifests: +//! - Android: AndroidManifest.xml with declarations +//! - iOS/macOS: Info.plist with usage description keys +//! +//! Other platforms (Linux, Web, Windows desktop) use runtime-only permissions +//! and do not require build-time manifest generation. +use permissions::Platform; +use serde::Serialize; + +/// Alias the shared manifest type from the permissions crate for CLI-specific helpers +pub type PermissionManifest = permissions::PermissionManifest; + +/// Android permission for Handlebars template +#[derive(Debug, Clone, Serialize)] +pub struct AndroidPermission { + pub name: String, + pub description: String, +} + +/// iOS permission for Handlebars template +#[derive(Debug, Clone, Serialize)] +pub struct IosPermission { + pub key: String, + pub description: String, +} + +/// macOS permission for Handlebars template +#[derive(Debug, Clone, Serialize)] +pub struct MacosPermission { + pub key: String, + pub description: String, +} + +/// Get Android permissions for Handlebars template +pub(crate) fn get_android_permissions(manifest: &PermissionManifest) -> Vec { + manifest + .permissions_for_platform(Platform::Android) + .iter() + .filter_map(|perm| { + perm.android_permission() + .map(|android_perm| AndroidPermission { + name: android_perm.to_string(), + description: perm.description().to_string(), + }) + }) + .collect() +} + +/// Get iOS permissions for Handlebars template +pub(crate) fn get_ios_permissions(manifest: &PermissionManifest) -> Vec { + manifest + .permissions_for_platform(Platform::Ios) + .iter() + .filter_map(|perm| { + perm.ios_key().map(|key| IosPermission { + key: key.to_string(), + description: perm.description().to_string(), + }) + }) + .collect() +} + +/// Get macOS permissions for Handlebars template +pub(crate) fn get_macos_permissions(manifest: &PermissionManifest) -> Vec { + manifest + .permissions_for_platform(Platform::Macos) + .iter() + .filter_map(|perm| { + perm.macos_key().map(|key| MacosPermission { + key: key.to_string(), + description: perm.description().to_string(), + }) + }) + .collect() +} + +/// Check if permissions are needed for the platform +#[allow(dead_code)] +pub(crate) fn needs_permission_manifest(platform: Platform) -> bool { + matches!( + platform, + Platform::Android | Platform::Ios | Platform::Macos + ) +} diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 1892a1e19f..1cde106e24 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -334,7 +334,7 @@ use dioxus_cli_opt::{process_file_to, AssetManifest}; use itertools::Itertools; use krates::{cm::TargetKind, NodeId}; use manganis::{AssetOptions, BundledAsset}; -use manganis_core::{AssetOptionsBuilder, AssetVariant}; +use manganis_core::AssetVariant; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, ffi::OsString}; @@ -390,6 +390,7 @@ pub(crate) struct BuildRequest { pub(crate) no_default_features: bool, pub(crate) target_dir: PathBuf, pub(crate) skip_assets: bool, + pub(crate) skip_permissions: bool, pub(crate) wasm_split: bool, pub(crate) debug_symbols: bool, pub(crate) inject_loading_scripts: bool, @@ -452,6 +453,9 @@ pub struct BuildArtifacts { pub(crate) time_start: SystemTime, pub(crate) time_end: SystemTime, pub(crate) assets: AssetManifest, + pub(crate) permissions: super::permissions::PermissionManifest, + pub(crate) android_artifacts: super::android_java::AndroidArtifactManifest, + pub(crate) swift_sources: super::ios_swift::SwiftSourceManifest, pub(crate) mode: BuildMode, pub(crate) patch_cache: Option>, pub(crate) depinfo: RustcDepInfo, @@ -931,7 +935,12 @@ impl BuildRequest { } // Make sure we set the sysroot for ios builds in the event the user doesn't have it set - if matches!(bundle, BundleFormat::Ios) { + if matches!(bundle, BundleFormat::Ios) + && matches!( + triple.operating_system, + target_lexicon::OperatingSystem::IOS(_) + ) + { let xcode_path = Workspace::get_xcode_path() .await .unwrap_or_else(|| "/Applications/Xcode.app".to_string().into()); @@ -1033,6 +1042,7 @@ impl BuildRequest { should_codesign, session_cache_dir, skip_assets: args.skip_assets, + skip_permissions: args.skip_permissions, base_path: args.base_path.clone(), wasm_split: args.wasm_split, debug_symbols: args.debug_symbols, @@ -1113,6 +1123,26 @@ impl BuildRequest { self.write_metadata() .await .context("Failed to write metadata")?; + + // Install prebuilt Android plugin artifacts (AARs + Gradle deps) + if self.bundle == BundleFormat::Android && !artifacts.android_artifacts.is_empty() { + self.install_android_artifacts(&artifacts.android_artifacts) + .context("Failed to install Android plugin artifacts")?; + } + + if matches!(self.bundle, BundleFormat::Ios | BundleFormat::MacOS) + && !artifacts.swift_sources.is_empty() + { + self.embed_swift_stdlibs(&artifacts.swift_sources) + .await + .context("Failed to embed Swift standard libraries")?; + } + + // Update platform manifests with permissions AFTER writing metadata + // to avoid having them overwritten by the template + self.update_manifests_with_permissions(&artifacts.permissions) + .context("Failed to update manifests with permissions")?; + self.optimize(ctx) .await .context("Failed to optimize build")?; @@ -1316,7 +1346,13 @@ impl BuildRequest { ); } - let assets = self.collect_assets(&exe, ctx).await?; + // Extract all linker metadata (assets, permissions, Android/iOS plugins) in a single pass. + let (assets, permissions, android_artifacts, swift_sources) = + self.collect_assets_and_permissions(&exe, ctx).await?; + + // Note: We'll update platform manifests with permissions AFTER write_metadata() + // to avoid having them overwritten by the template + let time_end = SystemTime::now(); let mode = ctx.mode.clone(); let depinfo = RustcDepInfo::from_file(&exe.with_extension("d")).unwrap_or_default(); @@ -1333,6 +1369,9 @@ impl BuildRequest { direct_rustc, time_start, assets, + permissions, + android_artifacts, + swift_sources, mode, depinfo, root_dir: self.root_dir(), @@ -1341,46 +1380,434 @@ impl BuildRequest { }) } - /// Collect the assets from the final executable and modify the binary in place to point to the right - /// hashed asset location. - async fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result { - // And then add from the exe directly, just in case it's LTO compiled and has no incremental cache - if self.skip_assets { - return Ok(AssetManifest::default()); + /// Collect both assets and permissions from the final executable in one pass + /// + /// This method combines both asset and permission extraction to read the binary + /// file only once, since both use the __ASSETS__ prefix. This avoids reading + /// the file twice and improves performance. + async fn collect_assets_and_permissions( + &self, + exe: &Path, + ctx: &BuildContext, + ) -> Result<( + AssetManifest, + super::permissions::PermissionManifest, + super::android_java::AndroidArtifactManifest, + super::ios_swift::SwiftSourceManifest, + )> { + use super::assets::extract_symbols_from_file; + + let skip_assets = self.skip_assets; + let skip_permissions = self.skip_permissions || self.bundle == BundleFormat::Web; + let needs_android_artifacts = self.bundle == BundleFormat::Android; + let needs_swift_packages = matches!(self.bundle, BundleFormat::Ios | BundleFormat::MacOS); + + if skip_assets && skip_permissions && !needs_android_artifacts && !needs_swift_packages { + return Ok(( + AssetManifest::default(), + super::permissions::PermissionManifest::default(), + super::android_java::AndroidArtifactManifest::default(), + super::ios_swift::SwiftSourceManifest::default(), + )); } ctx.status_extracting_assets(); + let super::assets::SymbolExtractionResult { + assets: extracted_assets, + permissions: extracted_permissions, + android_artifacts, + swift_packages, + } = extract_symbols_from_file(exe).await?; + + let asset_manifest = if skip_assets { + AssetManifest::default() + } else { + let mut manifest = AssetManifest::default(); + for asset in extracted_assets { + manifest.insert_asset(asset); + } - let mut manifest = super::assets::extract_assets_from_file(exe).await?; + if matches!(self.bundle, BundleFormat::Web) + && matches!(ctx.mode, BuildMode::Base { .. } | BuildMode::Fat) + { + if let Some(dir) = self.user_public_dir() { + for entry in walkdir::WalkDir::new(&dir) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + { + let from = entry.path().to_path_buf(); + let relative_path = from.strip_prefix(&dir).unwrap(); + let to = format!("../{}", relative_path.display()); + manifest.insert_asset(BundledAsset::new( + from.to_string_lossy().as_ref(), + to.as_str(), + manganis_core::AssetOptions::builder() + .with_hash_suffix(false) + .into_asset_options(), + )); + } + } + } - // If the user has a public dir, we submit all the entries there as assets too - // - // These don't receive a hash in their filename, since they're user-provided static assets - // We only do this for web builds - if matches!(self.bundle, BundleFormat::Web) - && matches!(ctx.mode, BuildMode::Base { .. } | BuildMode::Fat) - { - if let Some(dir) = self.user_public_dir() { - for entry in walkdir::WalkDir::new(&dir) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - { - let from = entry.path().to_path_buf(); - let relative_path = from.strip_prefix(&dir).unwrap(); - let to = format!("../{}", relative_path.display()); - manifest.insert_asset(BundledAsset::new( - from.to_string_lossy().as_ref(), - to.as_str(), - AssetOptionsBuilder::new() - .with_hash_suffix(false) - .into_asset_options(), - )); + manifest + }; + + let permission_manifest = if skip_permissions { + super::permissions::PermissionManifest::default() + } else { + let manifest = + super::permissions::PermissionManifest::from_permissions(extracted_permissions); + + let platform = match self.bundle { + BundleFormat::Android => Some(permissions::Platform::Android), + BundleFormat::Ios => Some(permissions::Platform::Ios), + BundleFormat::MacOS => Some(permissions::Platform::Macos), + _ => None, + }; + + if let Some(platform) = platform { + let perms = manifest.permissions_for_platform(platform); + if !perms.is_empty() { + tracing::info!("Found {} permissions for {:?}:", perms.len(), platform); + for perm in &perms { + tracing::debug!(" β€’ {:?} - {}", perm.kind(), perm.description()); + } + } else { + tracing::debug!("No permissions found for {:?}", platform); } + } else { + tracing::debug!( + "Skipping permission manifest generation for {:?} - uses runtime-only permissions", + self.bundle + ); + } + + manifest + }; + + let android_manifest = super::android_java::AndroidArtifactManifest::new(android_artifacts); + if !android_manifest.is_empty() { + tracing::debug!( + "Found {} Android artifact declaration(s)", + android_manifest.artifacts().len() + ); + for artifact in android_manifest.artifacts() { + tracing::debug!( + " Plugin: {} Artifact: {}", + artifact.plugin_name.as_str(), + artifact.artifact_path.as_str() + ); } } - Ok(manifest) + let swift_manifest = super::ios_swift::SwiftSourceManifest::new(swift_packages); + if !swift_manifest.is_empty() { + tracing::debug!( + "Found {} Swift package declaration(s) for {:?}", + swift_manifest.sources().len(), + self.bundle + ); + for source in swift_manifest.sources() { + tracing::debug!( + " Plugin: {} (Swift package path={} product={})", + source.plugin_name.as_str(), + source.package_path.as_str(), + source.product.as_str() + ); + } + } + + Ok(( + asset_manifest, + permission_manifest, + android_manifest, + swift_manifest, + )) + } + + /// Copy collected Android AARs into the Gradle project and add dependencies. + fn install_android_artifacts( + &self, + android_artifacts: &super::android_java::AndroidArtifactManifest, + ) -> Result<()> { + let libs_dir = self.root_dir().join("app").join("libs"); + std::fs::create_dir_all(&libs_dir)?; + + let build_gradle = self.root_dir().join("app").join("build.gradle.kts"); + for artifact in android_artifacts.artifacts() { + let artifact_path = PathBuf::from(artifact.artifact_path.as_str()); + if !artifact_path.exists() { + anyhow::bail!( + "Android plugin artifact not found: {}", + artifact_path.display() + ); + } + + let filename = artifact_path + .file_name() + .ok_or_else(|| { + anyhow::anyhow!( + "Android plugin artifact path has no filename: {}", + artifact_path.display() + ) + })? + .to_owned(); + let dest_file = libs_dir.join(&filename); + std::fs::copy(&artifact_path, &dest_file)?; + tracing::debug!( + "Copied Android artifact {} -> {}", + artifact_path.display(), + dest_file.display() + ); + + let dep_line = format!( + "implementation(files(\"libs/{}\"))", + filename.to_string_lossy() + ); + self.ensure_gradle_dependency(&build_gradle, &dep_line)?; + + for dependency in artifact + .gradle_dependencies + .as_str() + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + { + self.ensure_gradle_dependency(&build_gradle, dependency)?; + } + } + + Ok(()) + } + + /// Embed Swift standard libraries into the app bundle when Swift plugins are present. + async fn embed_swift_stdlibs( + &self, + swift_sources: &super::ios_swift::SwiftSourceManifest, + ) -> Result<()> { + if swift_sources.is_empty() { + return Ok(()); + } + + let platform_flag = match self.bundle { + BundleFormat::Ios => { + let triple_str = self.triple.to_string(); + if triple_str.contains("sim") || triple_str.contains("x86_64") { + "iphonesimulator" + } else { + "iphoneos" + } + } + BundleFormat::MacOS => "macosx", + _ => return Ok(()), + }; + + let frameworks_dir = self.frameworks_folder(); + std::fs::create_dir_all(&frameworks_dir)?; + + let exe_path = self.main_exe(); + if !exe_path.exists() { + anyhow::bail!( + "Expected executable at {} when embedding Swift stdlibs", + exe_path.display() + ); + } + + let output = Command::new("xcrun") + .arg("swift-stdlib-tool") + .arg("--copy") + .arg("--platform") + .arg(platform_flag) + .arg("--scan-executable") + .arg(&exe_path) + .arg("--destination") + .arg(&frameworks_dir) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + anyhow::bail!( + "swift-stdlib-tool failed: {}{}", + stderr.trim(), + if stdout.trim().is_empty() { + "".to_string() + } else { + format!(" | {}", stdout.trim()) + } + ); + } + + Ok(()) + } + + /// Update platform manifests with permissions after they're collected + pub(crate) fn update_manifests_with_permissions( + &self, + permissions: &super::permissions::PermissionManifest, + ) -> Result<()> { + match self.bundle { + BundleFormat::Android => self.update_android_manifest_with_permissions(permissions), + BundleFormat::Ios => self.update_ios_manifest_with_permissions(permissions), + BundleFormat::MacOS => self.update_macos_manifest_with_permissions(permissions), + _ => { + tracing::debug!( + "Skipping manifest update for {:?} - uses runtime-only permissions", + self.bundle + ); + Ok(()) + } + } + } + + fn update_android_manifest_with_permissions( + &self, + permissions: &super::permissions::PermissionManifest, + ) -> Result<()> { + let android_permissions = super::permissions::get_android_permissions(permissions); + if android_permissions.is_empty() { + tracing::debug!("No Android permissions found to add to manifest"); + return Ok(()); + } + + let manifest_path = self + .root_dir() + .join("app") + .join("src") + .join("main") + .join("AndroidManifest.xml"); + if !manifest_path.exists() { + tracing::warn!("AndroidManifest.xml not found, skipping permission update"); + return Ok(()); + } + + let mut manifest_content = std::fs::read_to_string(&manifest_path)?; + + // Find the position after the INTERNET permission + let internet_permission = + r#""#; + if let Some(pos) = manifest_content.find(internet_permission) { + let insert_pos = pos + internet_permission.len(); + + // Generate permission declarations + let mut permission_declarations = String::new(); + for perm in &android_permissions { + permission_declarations.push_str(&format!( + "\n ", + perm.name + )); + } + + manifest_content.insert_str(insert_pos, &permission_declarations); + std::fs::write(&manifest_path, manifest_content)?; + + tracing::debug!( + "Added {} Android permissions to AndroidManifest.xml", + android_permissions.len() + ); + for perm in &android_permissions { + tracing::debug!(" β€’ {} - {}", perm.name, perm.description); + } + } + + Ok(()) + } + + fn update_ios_manifest_with_permissions( + &self, + permissions: &super::permissions::PermissionManifest, + ) -> Result<()> { + let ios_permissions = super::permissions::get_ios_permissions(permissions); + if ios_permissions.is_empty() { + tracing::debug!("No iOS permissions found to add to manifest"); + return Ok(()); + } + + // For iOS, Info.plist is at the root of the .app bundle (not in Contents/) + let plist_path = self.root_dir().join("Info.plist"); + + if !plist_path.exists() { + tracing::debug!( + "Info.plist not found at {:?}, skipping permission update", + plist_path + ); + return Ok(()); + } + + let mut plist_content = std::fs::read_to_string(&plist_path)?; + + // Find the position before the closing + if let Some(pos) = plist_content.rfind("") { + let mut permission_entries = String::new(); + for perm in &ios_permissions { + permission_entries.push_str(&format!( + "\n\t{}\n\t{}", + perm.key, perm.description + )); + } + + plist_content.insert_str(pos, &permission_entries); + std::fs::write(&plist_path, plist_content)?; + + tracing::debug!( + "Added {} iOS permissions to Info.plist", + ios_permissions.len() + ); + for perm in &ios_permissions { + tracing::debug!(" β€’ {} - {}", perm.key, perm.description); + } + } + + Ok(()) + } + + fn update_macos_manifest_with_permissions( + &self, + permissions: &super::permissions::PermissionManifest, + ) -> Result<()> { + let macos_permissions = super::permissions::get_macos_permissions(permissions); + if macos_permissions.is_empty() { + tracing::debug!("No macOS permissions found to add to manifest"); + return Ok(()); + } + + // For macOS, Info.plist is at Contents/Info.plist inside the .app bundle + let plist_path = self.root_dir().join("Contents").join("Info.plist"); + if !plist_path.exists() { + tracing::warn!( + "Info.plist not found at {:?}, skipping permission update", + plist_path + ); + return Ok(()); + } + + let mut plist_content = std::fs::read_to_string(&plist_path)?; + + // Find the position before the closing + if let Some(pos) = plist_content.rfind("") { + let mut permission_entries = String::new(); + for perm in &macos_permissions { + permission_entries.push_str(&format!( + "\n\t{}\n\t{}", + perm.key, perm.description + )); + } + + plist_content.insert_str(pos, &permission_entries); + std::fs::write(&plist_path, plist_content)?; + + tracing::info!( + "πŸ–₯️ Added {} macOS permissions to Info.plist:", + macos_permissions.len() + ); + for perm in &macos_permissions { + tracing::info!(" β€’ {} - {}", perm.key, perm.description); + } + } + + Ok(()) } /// Take the output of rustc and make it into the main exe of the bundle @@ -1840,10 +2267,13 @@ impl BuildRequest { _ = std::fs::remove_file(PathBuf::from(args[idx + 1].as_str())); } - // Now extract the assets from the fat binary - artifacts.assets = self - .collect_assets(&self.patch_exe(artifacts.time_start), ctx) + // Now extract linker metadata from the fat binary (assets, permissions, plugin data) + let (assets, _permissions, android_artifacts, swift_sources) = self + .collect_assets_and_permissions(&self.patch_exe(artifacts.time_start), ctx) .await?; + artifacts.assets = assets; + artifacts.android_artifacts = android_artifacts; + artifacts.swift_sources = swift_sources; // If this is a web build, reset the index.html file in case it was modified by SSG self.write_index_html(&artifacts.assets) @@ -1927,7 +2357,11 @@ impl BuildRequest { || *arg == "-arch" || *arg == "-L" || *arg == "-target" - || *arg == "-isysroot" + || (*arg == "-isysroot" + && matches!( + self.triple.operating_system, + target_lexicon::OperatingSystem::IOS(_) + )) { out_args.push(arg.to_string()); out_args.push(original_args[idx + 1].to_string()); @@ -2014,8 +2448,13 @@ impl BuildRequest { } if let Some(vale) = extract_value("-isysroot") { - out_args.push("-isysroot".to_string()); - out_args.push(vale); + if matches!( + self.triple.operating_system, + target_lexicon::OperatingSystem::IOS(_) + ) { + out_args.push("-isysroot".to_string()); + out_args.push(vale); + } } Ok(out_args) @@ -2928,6 +3367,8 @@ impl BuildRequest { let target_cxx = tools.target_cxx(); let java_home = tools.java_home(); let ndk_home = tools.ndk.clone(); + let sdk_root = tools.sdk(); + let artifact_dir = self.android_artifact_dir()?; tracing::debug!( r#"Using android: min_sdk_version: {min_sdk_version} @@ -2936,14 +3377,37 @@ impl BuildRequest { target_cc: {target_cc:?} target_cxx: {target_cxx:?} java_home: {java_home:?} + sdk_root: {sdk_root:?} + artifact_dir: {artifact_dir:?} "# ); - if let Some(java_home) = java_home { + if let Some(java_home) = &java_home { tracing::debug!("Setting JAVA_HOME to {java_home:?}"); - env_vars.push(("JAVA_HOME".into(), java_home.into_os_string())); + env_vars.push(("JAVA_HOME".into(), java_home.clone().into_os_string())); + env_vars.push(( + "DX_ANDROID_JAVA_HOME".into(), + java_home.clone().into_os_string(), + )); } + env_vars.push(( + "DX_ANDROID_ARTIFACT_DIR".into(), + artifact_dir.into_os_string(), + )); + env_vars.push(( + "DX_ANDROID_NDK_HOME".into(), + ndk_home.clone().into_os_string(), + )); + env_vars.push(( + "DX_ANDROID_SDK_ROOT".into(), + sdk_root.clone().into_os_string(), + )); + env_vars.push(("ANDROID_NDK_HOME".into(), ndk_home.clone().into_os_string())); + env_vars.push(("ANDROID_SDK_ROOT".into(), sdk_root.clone().into_os_string())); + env_vars.push(("ANDROID_HOME".into(), sdk_root.into_os_string())); + env_vars.push(("NDK_HOME".into(), ndk_home.clone().into_os_string())); + let triple = self.triple.to_string(); // Environment variables for the `cc` crate @@ -3061,7 +3525,10 @@ impl BuildRequest { ), linker.into_os_string(), ), - ("ANDROID_NDK_ROOT".to_string(), ndk_home.into_os_string()), + ( + "ANDROID_NDK_ROOT".to_string(), + ndk_home.clone().into_os_string(), + ), ( "OPENSSL_LIB_DIR".to_string(), openssl_lib_dir.into_os_string(), @@ -3081,10 +3548,16 @@ impl BuildRequest { "WRY_ANDROID_LIBRARY".to_string(), "dioxusmain".to_string().into(), ), - ( - "WRY_ANDROID_KOTLIN_FILES_OUT_DIR".to_string(), - self.wry_android_kotlin_files_out_dir().into_os_string(), - ), + ("WRY_ANDROID_KOTLIN_FILES_OUT_DIR".to_string(), { + let kotlin_dir = self.wry_android_kotlin_files_out_dir(); + // Ensure the directory exists for WRY's canonicalize check + if let Err(e) = std::fs::create_dir_all(&kotlin_dir) { + tracing::error!("Failed to create kotlin directory {:?}: {}", kotlin_dir, e); + return Err(anyhow::anyhow!("Failed to create kotlin directory: {}", e)); + } + tracing::debug!("Created kotlin directory: {:?}", kotlin_dir); + kotlin_dir.into_os_string() + }), // Found this through a comment related to bindgen using the wrong clang for cross compiles // // https://github.com/rust-lang/rust-bindgen/issues/2962#issuecomment-2438297124 @@ -3107,6 +3580,17 @@ impl BuildRequest { Ok(env_vars) } + fn android_artifact_dir(&self) -> Result { + let dir = self + .internal_out_dir() + .join(&self.main_target) + .join(if self.release { "release" } else { "debug" }) + .join("android-artifacts") + .join(self.triple.to_string()); + std::fs::create_dir_all(&dir)?; + Ok(dir) + } + /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this /// will return an estimate of the number of units in the crate based on cargo metadata. /// @@ -3272,12 +3756,14 @@ impl BuildRequest { let app = root.join("app"); let app_main = app.join("src").join("main"); let app_kotlin = app_main.join("kotlin"); + let app_java = app_main.join("java"); let app_jnilibs = app_main.join("jniLibs"); let app_assets = app_main.join("assets"); let app_kotlin_out = self.wry_android_kotlin_files_out_dir(); create_dir_all(&app)?; create_dir_all(&app_main)?; create_dir_all(&app_kotlin)?; + create_dir_all(&app_java)?; create_dir_all(&app_jnilibs)?; create_dir_all(&app_assets)?; create_dir_all(&app_kotlin_out)?; @@ -3485,6 +3971,25 @@ impl BuildRequest { kotlin_dir } + fn ensure_gradle_dependency(&self, build_gradle: &Path, dependency_line: &str) -> Result<()> { + use std::fs; + + let mut contents = fs::read_to_string(build_gradle)?; + if contents.contains(dependency_line) { + return Ok(()); + } + + if let Some(idx) = contents.find("dependencies {") { + let insert_pos = idx + "dependencies {".len(); + contents.insert_str(insert_pos, &format!("\n {dependency_line}")); + } else { + contents.push_str(&format!("\ndependencies {{\n {dependency_line}\n}}\n")); + } + + fs::write(build_gradle, contents)?; + Ok(()) + } + /// Get the directory where this app can write to for this session that's guaranteed to be stable /// for the same app. This is useful for emitting state like window position and size. /// diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs index 5f8bcb485e..0368b604f3 100644 --- a/packages/cli/src/cli/target.rs +++ b/packages/cli/src/cli/target.rs @@ -89,6 +89,11 @@ pub(crate) struct TargetArgs { #[serde(default)] pub(crate) skip_assets: bool, + /// Skip collecting permissions from dependencies [default: false] + #[clap(long, help_heading = HELP_HEADING)] + #[serde(default)] + pub(crate) skip_permissions: bool, + /// Inject scripts to load the wasm and js files for your dioxus app if they are not already present [default: true] #[clap(long, default_value_t = true, help_heading = HELP_HEADING, num_args = 0..=1)] pub(crate) inject_loading_scripts: bool, diff --git a/packages/const-serialize-macro/Cargo.toml b/packages/const-serialize-macro/Cargo.toml index 8c20662ab1..123efc864b 100644 --- a/packages/const-serialize-macro/Cargo.toml +++ b/packages/const-serialize-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "const-serialize-macro" -version = { workspace = true } +version = "0.8.0" authors = ["Evan Almloff"] edition = "2021" description = "A macro to derive const serialize" diff --git a/packages/const-serialize-macro/src/lib.rs b/packages/const-serialize-macro/src/lib.rs index 11997c6b01..4d3c41fbfa 100644 --- a/packages/const-serialize-macro/src/lib.rs +++ b/packages/const-serialize-macro/src/lib.rs @@ -1,12 +1,12 @@ use proc_macro::TokenStream; use quote::{quote, ToTokens}; -use syn::{parse_macro_input, DeriveInput, LitInt}; +use syn::{parse_macro_input, DeriveInput, LitInt, Path}; use syn::{parse_quote, Generics, WhereClause, WherePredicate}; -fn add_bounds(where_clause: &mut Option, generics: &Generics) { +fn add_bounds(where_clause: &mut Option, generics: &Generics, krate: &Path) { let bounds = generics.params.iter().filter_map(|param| match param { syn::GenericParam::Type(ty) => { - Some::(parse_quote! { #ty: const_serialize::SerializeConst, }) + Some::(parse_quote! { #ty: #krate::SerializeConst, }) } syn::GenericParam::Lifetime(_) => None, syn::GenericParam::Const(_) => None, @@ -19,10 +19,33 @@ fn add_bounds(where_clause: &mut Option, generics: &Generics) { } /// Derive the const serialize trait for a struct -#[proc_macro_derive(SerializeConst)] -pub fn derive_parse(input: TokenStream) -> TokenStream { +#[proc_macro_derive(SerializeConst, attributes(const_serialize))] +pub fn derive_parse(raw_input: TokenStream) -> TokenStream { // Parse the input tokens into a syntax tree - let input = parse_macro_input!(input as DeriveInput); + let input = parse_macro_input!(raw_input as DeriveInput); + let krate = input.attrs.iter().find_map(|attr| { + attr.path() + .is_ident("const_serialize") + .then(|| { + let mut path = None; + if let Err(err) = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("crate") { + let ident: Path = meta.value()?.parse()?; + path = Some(ident); + } + Ok(()) + }) { + return Some(Err(err)); + } + path.map(Ok) + }) + .flatten() + }); + let krate = match krate { + Some(Ok(path)) => path, + Some(Err(err)) => return err.into_compile_error().into(), + None => parse_quote! { const_serialize }, + }; match input.data { syn::Data::Struct(data) => match data.fields { @@ -30,7 +53,7 @@ pub fn derive_parse(input: TokenStream) -> TokenStream { let ty = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let mut where_clause = where_clause.cloned(); - add_bounds(&mut where_clause, &input.generics); + add_bounds(&mut where_clause, &input.generics, &krate); let field_names = data.fields.iter().enumerate().map(|(i, field)| { field .ident @@ -43,13 +66,14 @@ pub fn derive_parse(input: TokenStream) -> TokenStream { }); let field_types = data.fields.iter().map(|field| &field.ty); quote! { - unsafe impl #impl_generics const_serialize::SerializeConst for #ty #ty_generics #where_clause { - const MEMORY_LAYOUT: const_serialize::Layout = const_serialize::Layout::Struct(const_serialize::StructLayout::new( + unsafe impl #impl_generics #krate::SerializeConst for #ty #ty_generics #where_clause { + const MEMORY_LAYOUT: #krate::Layout = #krate::Layout::Struct(#krate::StructLayout::new( std::mem::size_of::(), &[#( - const_serialize::StructFieldLayout::new( + #krate::StructFieldLayout::new( + stringify!(#field_names), std::mem::offset_of!(#ty, #field_names), - <#field_types as const_serialize::SerializeConst>::MEMORY_LAYOUT, + <#field_types as #krate::SerializeConst>::MEMORY_LAYOUT, ), )*], )); @@ -60,10 +84,10 @@ pub fn derive_parse(input: TokenStream) -> TokenStream { let ty = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let mut where_clause = where_clause.cloned(); - add_bounds(&mut where_clause, &input.generics); + add_bounds(&mut where_clause, &input.generics, &krate); quote! { - unsafe impl #impl_generics const_serialize::SerializeConst for #ty #ty_generics #where_clause { - const MEMORY_LAYOUT: const_serialize::Layout = const_serialize::Layout::Struct(const_serialize::StructLayout::new( + unsafe impl #impl_generics #krate::SerializeConst for #ty #ty_generics #where_clause { + const MEMORY_LAYOUT: #krate::Layout = #krate::Layout::Struct(#krate::StructLayout::new( std::mem::size_of::(), &[], )); @@ -137,7 +161,7 @@ pub fn derive_parse(input: TokenStream) -> TokenStream { let ty = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let mut where_clause = where_clause.cloned(); - add_bounds(&mut where_clause, &input.generics); + add_bounds(&mut where_clause, &input.generics, &krate); let mut last_discriminant = None; let variants = data.variants.iter().map(|variant| { let discriminant = variant @@ -151,6 +175,7 @@ pub fn derive_parse(input: TokenStream) -> TokenStream { } }); last_discriminant = Some(discriminant.clone()); + let variant_name = &variant.ident; let field_names = variant.fields.iter().enumerate().map(|(i, field)| { field .ident @@ -162,17 +187,19 @@ pub fn derive_parse(input: TokenStream) -> TokenStream { quote! { { #[allow(unused)] - #[derive(const_serialize::SerializeConst)] + #[derive(#krate::SerializeConst)] + #[const_serialize(crate = #krate)] #[repr(C)] struct VariantStruct #generics { #( #field_names: #field_types, )* } - const_serialize::EnumVariant::new( + #krate::EnumVariant::new( + stringify!(#variant_name), #discriminant as u32, - match VariantStruct::MEMORY_LAYOUT { - const_serialize::Layout::Struct(layout) => layout, + match ::MEMORY_LAYOUT { + #krate::Layout::Struct(layout) => layout, _ => panic!("VariantStruct::MEMORY_LAYOUT must be a struct"), }, ::std::mem::align_of::(), @@ -181,14 +208,14 @@ pub fn derive_parse(input: TokenStream) -> TokenStream { } }); quote! { - unsafe impl #impl_generics const_serialize::SerializeConst for #ty #ty_generics #where_clause { - const MEMORY_LAYOUT: const_serialize::Layout = const_serialize::Layout::Enum(const_serialize::EnumLayout::new( + unsafe impl #impl_generics #krate::SerializeConst for #ty #ty_generics #where_clause { + const MEMORY_LAYOUT: #krate::Layout = #krate::Layout::Enum(#krate::EnumLayout::new( ::std::mem::size_of::(), - const_serialize::PrimitiveLayout::new( + #krate::PrimitiveLayout::new( #discriminant_size as usize, ), { - const DATA: &'static [const_serialize::EnumVariant] = &[ + const DATA: &'static [#krate::EnumVariant] = &[ #( #variants, )* diff --git a/packages/const-serialize/Cargo.toml b/packages/const-serialize/Cargo.toml index 9d4b4e2647..567f7d1604 100644 --- a/packages/const-serialize/Cargo.toml +++ b/packages/const-serialize/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "const-serialize" -version = { workspace = true } +version = "0.8.0" authors = ["Evan Almloff"] edition = "2021" description = "A serialization framework that works in const contexts" @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/dioxuslabs/dioxus" homepage = "https://dioxuslabs.com/learn/0.5/getting_started" keywords = ["const", "serialize"] -rust-version = "1.80.0" +rust-version = "1.83.0" [dependencies] const-serialize-macro = { workspace = true } diff --git a/packages/const-serialize/README.md b/packages/const-serialize/README.md index dfa66de631..25c3a36c63 100644 --- a/packages/const-serialize/README.md +++ b/packages/const-serialize/README.md @@ -29,7 +29,7 @@ const { }; 3]; let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); - let buf = buf.read(); + let buf = buf.as_ref(); let (buf, deserialized) = match deserialize_const!([Struct; 3], buf) { Some(data) => data, None => panic!("data mismatch"), @@ -54,4 +54,4 @@ The rust [nomicon](https://doc.rust-lang.org/nomicon/data.html) defines the memo - Only constant sized types are supported. This means that you can't serialize a type like `Vec`. These types are difficult to create in const contexts in general - Only types with a well defined memory layout are supported (see and ). `repr(Rust)` enums don't have a well defined layout, so they are not supported. `repr(C, u8)` enums can be used instead -- Const rust does not support mutable references or points, so this crate leans heavily on function data structures for data processing. +- Const rust does not support mutable references or points, so this crate leans heavily on functional data structures for data processing. diff --git a/packages/const-serialize/src/array.rs b/packages/const-serialize/src/array.rs new file mode 100644 index 0000000000..c38b9356df --- /dev/null +++ b/packages/const-serialize/src/array.rs @@ -0,0 +1,64 @@ +use crate::*; + +/// The layout for a constant sized array. The array layout is just a length and an item layout. +#[derive(Debug, Copy, Clone)] +pub struct ArrayLayout { + pub(crate) len: usize, + pub(crate) item_layout: &'static Layout, +} + +impl ArrayLayout { + /// Create a new array layout + pub const fn new(len: usize, item_layout: &'static Layout) -> Self { + Self { len, item_layout } + } +} + +unsafe impl SerializeConst for [T; N] { + const MEMORY_LAYOUT: Layout = Layout::Array(ArrayLayout { + len: N, + item_layout: &T::MEMORY_LAYOUT, + }); +} + +/// Serialize a constant sized array that is stored at the pointer passed in +pub(crate) const unsafe fn serialize_const_array( + ptr: *const (), + mut to: ConstVec, + layout: &ArrayLayout, +) -> ConstVec { + let len = layout.len; + let mut i = 0; + to = write_array(to, len); + while i < len { + let field = ptr.wrapping_byte_offset((i * layout.item_layout.size()) as _); + to = serialize_const_ptr(field, to, layout.item_layout); + i += 1; + } + to +} + +/// Deserialize an array type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. +pub(crate) const fn deserialize_const_array<'a>( + from: &'a [u8], + layout: &ArrayLayout, + mut out: &mut [MaybeUninit], +) -> Option<&'a [u8]> { + let item_layout = layout.item_layout; + let Ok((_, mut from)) = take_array(from) else { + return None; + }; + let mut i = 0; + while i < layout.len { + let Some(new_from) = deserialize_const_ptr(from, item_layout, out) else { + return None; + }; + let Some((_, item_out)) = out.split_at_mut_checked(item_layout.size()) else { + return None; + }; + out = item_out; + from = new_from; + i += 1; + } + Some(from) +} diff --git a/packages/const-serialize/src/cbor.rs b/packages/const-serialize/src/cbor.rs new file mode 100644 index 0000000000..bc37cc1759 --- /dev/null +++ b/packages/const-serialize/src/cbor.rs @@ -0,0 +1,597 @@ +//! Const serialization utilities for the CBOR data format. +//! +//! ## Overview of the format +//! +//! Const serialize only supports a subset of the CBOR format, specifically the major types: +//! - UnsignedInteger +//! - NegativeInteger +//! - Bytes +//! - String +//! - Array +//! +//! Each item in CBOR starts with a leading byte, which determines the type of the item and additional information. +//! The additional information is encoded in the lower 5 bits of the leading byte and generally indicates either a +//! small number or how many of the next bytes are part of the first number. +//! +//! Resources: +//! The spec: +//! A playground to check examples against: + +use crate::ConstVec; + +/// Each item in CBOR starts with a leading byte, which determines the type of the item and additional information. +/// +/// The first 3 bits of the leading byte are the major type, which indicates the type of the item. +#[repr(u8)] +#[derive(PartialEq)] +enum MajorType { + /// An unsigned integer in the range 0..2^64. The value of the number is encoded in the remaining bits of the leading byte and any additional bytes. + UnsignedInteger = 0, + /// An unsigned integer in the range -2^64..-1. The value of the number is encoded in the remaining bits of the leading byte and any additional bytes + NegativeInteger = 1, + /// A byte sequence. The number of bytes in the sequence is encoded in the remaining bits of the leading byte and any additional bytes. + Bytes = 2, + /// A text sequence. The number of bytes in the sequence is encoded in the remaining bits of the leading byte and any additional bytes. + Text = 3, + /// A dynamically sized array of non-uniform data items. The number of items in the array is encoded in the remaining bits of the leading byte and any additional bytes. + Array = 4, + /// A map of pairs of data items. The first item in each pair is the key and the second item is the value. The number of items in the array is encoded in the remaining bits of the leading byte and any additional bytes. + Map = 5, + /// Tagged values - not supported + Tagged = 6, + /// Floating point values - not supported + Float = 7, +} + +impl MajorType { + /// The bitmask for the major type in the leading byte + const MASK: u8 = 0b0001_1111; + + const fn from_byte(byte: u8) -> Self { + match byte >> 5 { + 0 => MajorType::UnsignedInteger, + 1 => MajorType::NegativeInteger, + 2 => MajorType::Bytes, + 3 => MajorType::Text, + 4 => MajorType::Array, + 5 => MajorType::Map, + 6 => MajorType::Tagged, + 7 => MajorType::Float, + _ => panic!("Invalid major type"), + } + } +} + +/// Get the length of the item in bytes without deserialization. +const fn item_length(bytes: &[u8]) -> Result { + let [head, rest @ ..] = bytes else { + return Err(()); + }; + let major = MajorType::from_byte(*head); + let additional_information = *head & MajorType::MASK; + let length_of_item = match major { + // The length of the number is the total of: + // - The length of the number (which may be 0 if the number is encoded in additional information) + MajorType::UnsignedInteger | MajorType::NegativeInteger => { + get_length_of_number(additional_information) as usize + } + // The length of the text or bytes is the total of: + // - The length of the number that denotes the length of the text or bytes + // - The length of the text or bytes themselves + MajorType::Text | MajorType::Bytes => { + let length_of_number = get_length_of_number(additional_information); + let Ok((length_of_bytes, _)) = + grab_u64_with_byte_length(rest, length_of_number, additional_information) + else { + return Err(()); + }; + length_of_number as usize + length_of_bytes as usize + } + // The length of the map is the total of: + // - The length of the number that denotes the number of items + // - The length of the pairs of items themselves + MajorType::Array | MajorType::Map => { + let length_of_number = get_length_of_number(additional_information); + let Ok((length_of_items, _)) = + grab_u64_with_byte_length(rest, length_of_number, additional_information) + else { + return Err(()); + }; + let mut total_length = length_of_number as usize; + let mut items_left = length_of_items * if let MajorType::Map = major { 2 } else { 1 }; + while items_left > 0 { + let Some((_, after)) = rest.split_at_checked(total_length) else { + return Err(()); + }; + let Ok(item_length) = item_length(after) else { + return Err(()); + }; + total_length += item_length; + items_left -= 1; + } + total_length + } + _ => return Err(()), + }; + let length_of_head = 1; + Ok(length_of_head + length_of_item) +} + +/// Read a number from the buffer, returning the number and the remaining bytes. +pub(crate) const fn take_number(bytes: &[u8]) -> Result<(i64, &[u8]), ()> { + let [head, rest @ ..] = bytes else { + return Err(()); + }; + let major = MajorType::from_byte(*head); + let additional_information = *head & MajorType::MASK; + match major { + MajorType::UnsignedInteger => { + let Ok((number, rest)) = grab_u64(rest, additional_information) else { + return Err(()); + }; + Ok((number as i64, rest)) + } + MajorType::NegativeInteger => { + let Ok((number, rest)) = grab_u64(rest, additional_information) else { + return Err(()); + }; + Ok((-(1 + number as i64), rest)) + } + _ => Err(()), + } +} + +/// Write a number to the buffer +pub(crate) const fn write_number( + vec: ConstVec, + number: i64, +) -> ConstVec { + match number { + 0.. => write_major_type_and_u64(vec, MajorType::UnsignedInteger, number as u64), + ..0 => write_major_type_and_u64(vec, MajorType::NegativeInteger, (-(number + 1)) as u64), + } +} + +/// Write the major type along with a number to the buffer. The first byte +/// contains both the major type and the additional information which contains +/// either the number itself or the number of extra bytes the number occupies. +const fn write_major_type_and_u64( + vec: ConstVec, + major: MajorType, + number: u64, +) -> ConstVec { + let major = (major as u8) << 5; + match number { + // For numbers less than 24, store the number in the lower bits + // of the first byte + 0..24 => { + let additional_information = number as u8; + let byte = major | additional_information; + vec.push(byte) + } + // For larger numbers, store the number of extra bytes the number occupies + 24.. => { + let log2_additional_bytes = log2_bytes_for_number(number); + let additional_bytes = 1 << log2_additional_bytes; + let additional_information = log2_additional_bytes + 24; + let byte = major | additional_information; + let mut vec = vec.push(byte); + let mut byte = 0; + while byte < additional_bytes { + vec = vec.push((number >> ((additional_bytes - byte - 1) * 8)) as u8); + byte += 1; + } + vec + } + } +} + +/// Find the number of bytes required to store a number and return the log2 of the number of bytes. +/// This is the number stored in the additional information field if the number is more than 24. +const fn log2_bytes_for_number(number: u64) -> u8 { + let required_bytes = ((64 - number.leading_zeros()).div_ceil(8)) as u8; + #[allow(clippy::match_overlapping_arm)] + match required_bytes { + ..=1 => 0, + ..=2 => 1, + ..=4 => 2, + _ => 3, + } +} + +/// Take bytes from a slice and return the bytes and the remaining slice. +pub(crate) const fn take_bytes(bytes: &[u8]) -> Result<(&[u8], &[u8]), ()> { + let [head, rest @ ..] = bytes else { + return Err(()); + }; + let major = MajorType::from_byte(*head); + let additional_information = *head & MajorType::MASK; + if let MajorType::Bytes = major { + take_bytes_from(rest, additional_information) + } else { + Err(()) + } +} + +/// Write bytes to a buffer and return the new buffer. +pub(crate) const fn write_bytes( + vec: ConstVec, + bytes: &[u8], +) -> ConstVec { + let vec = write_major_type_and_u64(vec, MajorType::Bytes, bytes.len() as u64); + vec.extend(bytes) +} + +/// Take a string from a buffer and return the string and the remaining buffer. +pub(crate) const fn take_str(bytes: &[u8]) -> Result<(&str, &[u8]), ()> { + let [head, rest @ ..] = bytes else { + return Err(()); + }; + let major = MajorType::from_byte(*head); + let additional_information = *head & MajorType::MASK; + if let MajorType::Text = major { + let Ok((bytes, rest)) = take_bytes_from(rest, additional_information) else { + return Err(()); + }; + let Ok(string) = std::str::from_utf8(bytes) else { + return Err(()); + }; + Ok((string, rest)) + } else { + Err(()) + } +} + +/// Write a string to a buffer and return the new buffer. +pub(crate) const fn write_str( + vec: ConstVec, + string: &str, +) -> ConstVec { + let vec = write_major_type_and_u64(vec, MajorType::Text, string.len() as u64); + vec.extend(string.as_bytes()) +} + +/// Take the length and header of an array from a buffer and return the length and the remaining buffer. +/// You must loop over the elements of the array and parse them outside of this method. +pub(crate) const fn take_array(bytes: &[u8]) -> Result<(usize, &[u8]), ()> { + let [head, rest @ ..] = bytes else { + return Err(()); + }; + let major = MajorType::from_byte(*head); + let additional_information = *head & MajorType::MASK; + if let MajorType::Array = major { + let Ok((length, rest)) = take_len_from(rest, additional_information) else { + return Err(()); + }; + Ok((length as usize, rest)) + } else { + Err(()) + } +} + +/// Write the header and length of an array. +pub(crate) const fn write_array( + vec: ConstVec, + len: usize, +) -> ConstVec { + write_major_type_and_u64(vec, MajorType::Array, len as u64) +} + +/// Write the header and length of a map. +pub(crate) const fn write_map( + vec: ConstVec, + len: usize, +) -> ConstVec { + // We write 2 * len as the length of the map because each key-value pair is a separate entry. + write_major_type_and_u64(vec, MajorType::Map, len as u64) +} + +/// Write the key of a map entry. +pub(crate) const fn write_map_key( + value: ConstVec, + key: &str, +) -> ConstVec { + write_str(value, key) +} + +/// Take a map from the byte slice and return the map reference and the remaining bytes. +pub(crate) const fn take_map<'a>(bytes: &'a [u8]) -> Result<(MapRef<'a>, &'a [u8]), ()> { + let [head, rest @ ..] = bytes else { + return Err(()); + }; + let major = MajorType::from_byte(*head); + let additional_information = *head & MajorType::MASK; + if let MajorType::Map = major { + let Ok((length, rest)) = take_len_from(rest, additional_information) else { + return Err(()); + }; + let mut after_map = rest; + let mut items_left = length * 2; + while items_left > 0 { + // Skip the value + let Ok(len) = item_length(after_map) else { + return Err(()); + }; + let Some((_, rest)) = after_map.split_at_checked(len) else { + return Err(()); + }; + after_map = rest; + items_left -= 1; + } + Ok((MapRef::new(rest, length as usize), after_map)) + } else { + Err(()) + } +} + +/// A reference to a CBOR map. +pub(crate) struct MapRef<'a> { + /// The bytes of the map. + pub(crate) bytes: &'a [u8], + /// The length of the map. + pub(crate) len: usize, +} + +impl<'a> MapRef<'a> { + /// Create a new map reference. + const fn new(bytes: &'a [u8], len: usize) -> Self { + Self { bytes, len } + } + + /// Find a key in the map and return the buffer associated with it. + pub(crate) const fn find(&self, key: &str) -> Result, ()> { + let mut bytes = self.bytes; + let mut items_left = self.len; + while items_left > 0 { + let Ok((str, rest)) = take_str(bytes) else { + return Err(()); + }; + if str_eq(key, str) { + return Ok(Some(rest)); + } + // Skip the value associated with the key we don't care about + let Ok(len) = item_length(rest) else { + return Err(()); + }; + let Some((_, rest)) = rest.split_at_checked(len) else { + return Err(()); + }; + bytes = rest; + items_left -= 1; + } + Ok(None) + } +} + +/// Compare two strings for equality at compile time. +pub(crate) const fn str_eq(a: &str, b: &str) -> bool { + let a_bytes = a.as_bytes(); + let b_bytes = b.as_bytes(); + let a_len = a_bytes.len(); + let b_len = b_bytes.len(); + if a_len != b_len { + return false; + } + let mut index = 0; + while index < a_len { + if a_bytes[index] != b_bytes[index] { + return false; + } + index += 1; + } + true +} + +/// Take the length from the additional information byte and return it along with the remaining bytes. +const fn take_len_from(rest: &[u8], additional_information: u8) -> Result<(u64, &[u8]), ()> { + match additional_information { + // If additional_information < 24, the argument's value is the value of the additional information. + 0..24 => Ok((additional_information as u64, rest)), + // If additional_information is between 24 and 28, the argument's value is held in the n following bytes. + 24..28 => { + let Ok((number, rest)) = grab_u64(rest, additional_information) else { + return Err(()); + }; + Ok((number, rest)) + } + _ => Err(()), + } +} + +/// Take a list of bytes from the byte slice and the additional information byte +/// and return the bytes and the remaining bytes. +pub(crate) const fn take_bytes_from( + rest: &[u8], + additional_information: u8, +) -> Result<(&[u8], &[u8]), ()> { + let Ok((number, rest)) = grab_u64(rest, additional_information) else { + return Err(()); + }; + let Some((bytes, rest)) = rest.split_at_checked(number as usize) else { + return Err(()); + }; + Ok((bytes, rest)) +} + +/// Find the length of the number based on the additional information byte. +const fn get_length_of_number(additional_information: u8) -> u8 { + match additional_information { + 0..24 => 0, + 24..28 => 1 << (additional_information - 24), + _ => 0, + } +} + +/// Read a u64 from the byte slice and the additional information byte. +const fn grab_u64(rest: &[u8], additional_information: u8) -> Result<(u64, &[u8]), ()> { + grab_u64_with_byte_length( + rest, + get_length_of_number(additional_information), + additional_information, + ) +} + +/// Read a u64 from the byte slice and the additional information byte along with the byte length. +const fn grab_u64_with_byte_length( + mut rest: &[u8], + byte_length: u8, + additional_information: u8, +) -> Result<(u64, &[u8]), ()> { + match byte_length { + 0 => Ok((additional_information as u64, rest)), + n => { + let mut value = 0; + let mut count = 0; + while count < n { + let [next, remaining @ ..] = rest else { + return Err(()); + }; + value = (value << 8) | *next as u64; + rest = remaining; + count += 1; + } + Ok((value, rest)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_byte() { + for byte in 0..=255 { + let bytes = if byte < 24 { [byte, 0] } else { [24, byte] }; + let (item, _) = take_number(&bytes).unwrap(); + assert_eq!(item, byte as _); + } + for byte in 1..=255 { + let bytes = if byte < 24 { + [(byte - 1) | 0b0010_0000, 0] + } else { + [0b0010_0000 | 24, byte - 1] + }; + let (item, _) = take_number(&bytes).unwrap(); + assert_eq!(item, -(byte as i64)); + } + } + + #[test] + fn test_byte_roundtrip() { + for byte in 0..=255 { + let vec = write_number(ConstVec::new(), byte as _); + println!("{vec:?}"); + let (item, _) = take_number(vec.as_ref()).unwrap(); + assert_eq!(item, byte as _); + } + for byte in 0..=255 { + let vec = write_number(ConstVec::new(), -(byte as i64)); + let (item, _) = take_number(vec.as_ref()).unwrap(); + assert_eq!(item, -(byte as i64)); + } + } + + #[test] + fn test_number_roundtrip() { + for _ in 0..100 { + let value = rand::random::(); + let vec = write_number(ConstVec::new(), value); + let (item, _) = take_number(vec.as_ref()).unwrap(); + assert_eq!(item, value); + } + } + + #[test] + fn test_bytes_roundtrip() { + for _ in 0..100 { + let len = (rand::random::() % 100) as usize; + let bytes = rand::random::<[u8; 100]>(); + let vec = write_bytes(ConstVec::new(), &bytes[..len]); + let (item, _) = take_bytes(vec.as_ref()).unwrap(); + assert_eq!(item, &bytes[..len]); + } + } + + #[test] + fn test_array_roundtrip() { + for _ in 0..100 { + let len = (rand::random::() % 100) as usize; + let mut vec = write_array(ConstVec::new(), len); + for i in 0..len { + vec = write_number(vec, i as _); + } + let (len, mut remaining) = take_array(vec.as_ref()).unwrap(); + for i in 0..len { + let (item, rest) = take_number(remaining).unwrap(); + remaining = rest; + assert_eq!(item, i as i64); + } + } + } + + #[test] + fn test_map_roundtrip() { + use rand::prelude::SliceRandom; + for _ in 0..100 { + let len = (rand::random::() % 10) as usize; + let mut vec = write_map(ConstVec::new(), len); + let mut random_order_indexes = (0..len).collect::>(); + random_order_indexes.shuffle(&mut rand::rng()); + for &i in &random_order_indexes { + vec = write_map_key(vec, &i.to_string()); + vec = write_number(vec, i as _); + } + println!("len: {}", len); + println!("Map: {:?}", vec); + let (map, remaining) = take_map(vec.as_ref()).unwrap(); + println!("remaining: {:?}", remaining); + assert!(remaining.is_empty()); + for i in 0..len { + let key = i.to_string(); + let key_location = map + .find(&key) + .expect("encoding is valid") + .expect("key exists"); + let (value, _) = take_number(key_location).unwrap(); + assert_eq!(value, i as i64); + } + } + } + + #[test] + fn test_item_length_str() { + #[rustfmt::skip] + let input = [ + /* text(1) */ 0x61, + /* "1" */ 0x31, + /* text(1) */ 0x61, + /* "1" */ 0x31, + ]; + let Ok(length) = item_length(&input) else { + panic!("Failed to calculate length"); + }; + assert_eq!(length, 2); + } + + #[test] + fn test_item_length_map() { + #[rustfmt::skip] + let input = [ + /* map(1) */ 0xA1, + /* text(1) */ 0x61, + /* "A" */ 0x41, + /* map(2) */ 0xA2, + /* text(3) */ 0x63, + /* "one" */ 0x6F, 0x6E, 0x65, + /* unsigned(286331153) */ 0x1A, 0x11, 0x11, 0x11, 0x11, + /* text(3) */ 0x63, + /* "two" */ 0x74, 0x77, 0x6F, + /* unsigned(34) */ 0x18, 0x22, + ]; + let Ok(length) = item_length(&input) else { + panic!("Failed to calculate length"); + }; + assert_eq!(length, input.len()); + } +} diff --git a/packages/const-serialize/src/const_buffers.rs b/packages/const-serialize/src/const_buffers.rs deleted file mode 100644 index 4e93ddbdbc..0000000000 --- a/packages/const-serialize/src/const_buffers.rs +++ /dev/null @@ -1,38 +0,0 @@ -/// A buffer that can be read from at compile time. This is very similar to [Cursor](std::io::Cursor) but is -/// designed to be used in const contexts. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct ConstReadBuffer<'a> { - location: usize, - memory: &'a [u8], -} - -impl<'a> ConstReadBuffer<'a> { - /// Create a new buffer from a byte slice - pub const fn new(memory: &'a [u8]) -> Self { - Self { - location: 0, - memory, - } - } - - /// Get the next byte from the buffer. Returns `None` if the buffer is empty. - /// This will return the new version of the buffer with the first byte removed. - pub const fn get(mut self) -> Option<(Self, u8)> { - if self.location >= self.memory.len() { - return None; - } - let value = self.memory[self.location]; - self.location += 1; - Some((self, value)) - } - - /// Get a reference to the underlying byte slice - pub const fn as_ref(&self) -> &[u8] { - self.memory - } - - /// Get a slice of the buffer from the current location to the end of the buffer - pub const fn remaining(&self) -> &[u8] { - self.memory.split_at(self.location).1 - } -} diff --git a/packages/const-serialize/src/const_vec.rs b/packages/const-serialize/src/const_vec.rs index 4c3c9a4a2a..5b618bd80c 100644 --- a/packages/const-serialize/src/const_vec.rs +++ b/packages/const-serialize/src/const_vec.rs @@ -1,8 +1,6 @@ #![allow(dead_code)] use std::{fmt::Debug, hash::Hash, mem::MaybeUninit}; -use crate::ConstReadBuffer; - const DEFAULT_MAX_SIZE: usize = 2usize.pow(10); /// [`ConstVec`] is a version of [`Vec`] that is usable in const contexts. It has @@ -327,22 +325,6 @@ impl ConstVec { } } -impl ConstVec { - /// Convert the [`ConstVec`] into a [`ConstReadBuffer`] - /// - /// # Example - /// ```rust - /// # use const_serialize::{ConstVec, ConstReadBuffer}; - /// const EMPTY: ConstVec = ConstVec::new(); - /// const ONE: ConstVec = EMPTY.push(1); - /// const TWO: ConstVec = ONE.push(2); - /// const READ: ConstReadBuffer = TWO.read(); - /// ``` - pub const fn read(&self) -> ConstReadBuffer<'_> { - ConstReadBuffer::new(self.as_ref()) - } -} - #[test] fn test_const_vec() { const VEC: ConstVec = { diff --git a/packages/const-serialize/src/enum.rs b/packages/const-serialize/src/enum.rs new file mode 100644 index 0000000000..953af21474 --- /dev/null +++ b/packages/const-serialize/src/enum.rs @@ -0,0 +1,135 @@ +use crate::*; + +/// Serialize an enum that is stored at the pointer passed in +pub(crate) const unsafe fn serialize_const_enum( + ptr: *const (), + mut to: ConstVec, + layout: &EnumLayout, +) -> ConstVec { + let byte_ptr = ptr as *const u8; + let discriminant = layout.discriminant.read(byte_ptr); + + let mut i = 0; + while i < layout.variants.len() { + // If the variant is the discriminated one, serialize it + let EnumVariant { + tag, name, data, .. + } = &layout.variants[i]; + if discriminant == *tag { + to = write_map(to, 1); + to = write_map_key(to, name); + let data_ptr = ptr.wrapping_byte_offset(layout.variants_offset as _); + to = serialize_const_struct(data_ptr, to, data); + break; + } + i += 1; + } + to +} + +/// The layout for an enum. The enum layout is just a discriminate size and a tag layout. +#[derive(Debug, Copy, Clone)] +pub struct EnumLayout { + pub(crate) size: usize, + discriminant: PrimitiveLayout, + variants_offset: usize, + variants: &'static [EnumVariant], +} + +impl EnumLayout { + /// Create a new enum layout + pub const fn new( + size: usize, + discriminant: PrimitiveLayout, + variants: &'static [EnumVariant], + ) -> Self { + let mut max_align = 1; + let mut i = 0; + while i < variants.len() { + let EnumVariant { align, .. } = &variants[i]; + if *align > max_align { + max_align = *align; + } + i += 1; + } + + let variants_offset_raw = discriminant.size; + let padding = (max_align - (variants_offset_raw % max_align)) % max_align; + let variants_offset = variants_offset_raw + padding; + + assert!(variants_offset % max_align == 0); + + Self { + size, + discriminant, + variants_offset, + variants, + } + } +} + +/// The layout for an enum variant. The enum variant layout is just a struct layout with a tag and alignment. +#[derive(Debug, Copy, Clone)] +pub struct EnumVariant { + name: &'static str, + // Note: tags may not be sequential + tag: u32, + data: StructLayout, + align: usize, +} + +impl EnumVariant { + /// Create a new enum variant layout + pub const fn new(name: &'static str, tag: u32, data: StructLayout, align: usize) -> Self { + Self { + name, + tag, + data, + align, + } + } +} + +/// Deserialize an enum type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. +pub(crate) const fn deserialize_const_enum<'a>( + from: &'a [u8], + layout: &EnumLayout, + out: &mut [MaybeUninit], +) -> Option<&'a [u8]> { + // First, deserialize the map + let Ok((map, remaining)) = take_map(from) else { + return None; + }; + + // Then get the only field which is the tag + let Ok((deserilized_name, from)) = take_str(map.bytes) else { + return None; + }; + + // Then, deserialize the variant + let mut i = 0; + let mut matched_variant = false; + while i < layout.variants.len() { + // If the variant is the discriminated one, deserialize it + let EnumVariant { + name, data, tag, .. + } = &layout.variants[i]; + if str_eq(deserilized_name, name) { + layout.discriminant.write(*tag, out); + let Some((_, out)) = out.split_at_mut_checked(layout.variants_offset) else { + return None; + }; + if deserialize_const_struct(from, data, out).is_none() { + return None; + } + matched_variant = true; + break; + } + i += 1; + } + if !matched_variant { + return None; + } + + Some(remaining) +} diff --git a/packages/const-serialize/src/lib.rs b/packages/const-serialize/src/lib.rs index 4cc5dcff1a..fa89945ea5 100644 --- a/packages/const-serialize/src/lib.rs +++ b/packages/const-serialize/src/lib.rs @@ -1,126 +1,30 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs)] -use std::{char, mem::MaybeUninit}; +use std::mem::MaybeUninit; -mod const_buffers; +mod cbor; mod const_vec; +mod r#enum; +pub use r#enum::*; +mod r#struct; +pub use r#struct::*; +mod primitive; +pub use primitive::*; +mod list; +pub use list::*; +mod array; +pub use array::*; +mod str; +pub use str::*; -pub use const_buffers::ConstReadBuffer; pub use const_serialize_macro::SerializeConst; pub use const_vec::ConstVec; -/// Plain old data for a field. Stores the offset of the field in the struct and the layout of the field. -#[derive(Debug, Copy, Clone)] -pub struct StructFieldLayout { - offset: usize, - layout: Layout, -} - -impl StructFieldLayout { - /// Create a new struct field layout - pub const fn new(offset: usize, layout: Layout) -> Self { - Self { offset, layout } - } -} - -/// Layout for a struct. The struct layout is just a list of fields with offsets -#[derive(Debug, Copy, Clone)] -pub struct StructLayout { - size: usize, - data: &'static [StructFieldLayout], -} - -impl StructLayout { - /// Create a new struct layout - pub const fn new(size: usize, data: &'static [StructFieldLayout]) -> Self { - Self { size, data } - } -} - -/// The layout for an enum. The enum layout is just a discriminate size and a tag layout. -#[derive(Debug, Copy, Clone)] -pub struct EnumLayout { - size: usize, - discriminant: PrimitiveLayout, - variants_offset: usize, - variants: &'static [EnumVariant], -} - -impl EnumLayout { - /// Create a new enum layout - pub const fn new( - size: usize, - discriminant: PrimitiveLayout, - variants: &'static [EnumVariant], - ) -> Self { - let mut max_align = 1; - let mut i = 0; - while i < variants.len() { - let EnumVariant { align, .. } = &variants[i]; - if *align > max_align { - max_align = *align; - } - i += 1; - } - - let variants_offset_raw = discriminant.size; - let padding = (max_align - (variants_offset_raw % max_align)) % max_align; - let variants_offset = variants_offset_raw + padding; - - assert!(variants_offset % max_align == 0); - - Self { - size, - discriminant, - variants_offset, - variants, - } - } -} - -/// The layout for an enum variant. The enum variant layout is just a struct layout with a tag and alignment. -#[derive(Debug, Copy, Clone)] -pub struct EnumVariant { - // Note: tags may not be sequential - tag: u32, - data: StructLayout, - align: usize, -} - -impl EnumVariant { - /// Create a new enum variant layout - pub const fn new(tag: u32, data: StructLayout, align: usize) -> Self { - Self { tag, data, align } - } -} - -/// The layout for a constant sized array. The array layout is just a length and an item layout. -#[derive(Debug, Copy, Clone)] -pub struct ListLayout { - len: usize, - item_layout: &'static Layout, -} - -impl ListLayout { - /// Create a new list layout - pub const fn new(len: usize, item_layout: &'static Layout) -> Self { - Self { len, item_layout } - } -} - -/// The layout for a primitive type. The bytes will be reversed if the target is big endian. -#[derive(Debug, Copy, Clone)] -pub struct PrimitiveLayout { - size: usize, -} - -impl PrimitiveLayout { - /// Create a new primitive layout - pub const fn new(size: usize) -> Self { - Self { size } - } -} +use crate::cbor::{ + str_eq, take_array, take_bytes, take_map, take_number, take_str, write_array, write_bytes, + write_map, write_map_key, write_number, +}; /// The layout for a type. This layout defines a sequence of locations and reversed or not bytes. These bytes will be copied from during serialization and copied into during deserialization. #[derive(Debug, Copy, Clone)] @@ -129,10 +33,12 @@ pub enum Layout { Enum(EnumLayout), /// A struct layout Struct(StructLayout), - /// A list layout - List(ListLayout), + /// An array layout + Array(ArrayLayout), /// A primitive layout Primitive(PrimitiveLayout), + /// A dynamically sized list layout + List(ListLayout), } impl Layout { @@ -141,7 +47,8 @@ impl Layout { match self { Layout::Enum(layout) => layout.size, Layout::Struct(layout) => layout.size, - Layout::List(layout) => layout.len * layout.item_layout.size(), + Layout::Array(layout) => layout.len * layout.item_layout.size(), + Layout::List(layout) => layout.size, Layout::Primitive(layout) => layout.size, } } @@ -158,533 +65,16 @@ pub unsafe trait SerializeConst: Sized { const _ASSERT: () = assert!(Self::MEMORY_LAYOUT.size() == std::mem::size_of::()); } -macro_rules! impl_serialize_const { - ($type:ty) => { - unsafe impl SerializeConst for $type { - const MEMORY_LAYOUT: Layout = Layout::Primitive(PrimitiveLayout { - size: std::mem::size_of::<$type>(), - }); - } - }; -} - -impl_serialize_const!(u8); -impl_serialize_const!(u16); -impl_serialize_const!(u32); -impl_serialize_const!(u64); -impl_serialize_const!(i8); -impl_serialize_const!(i16); -impl_serialize_const!(i32); -impl_serialize_const!(i64); -impl_serialize_const!(bool); -impl_serialize_const!(f32); -impl_serialize_const!(f64); - -unsafe impl SerializeConst for [T; N] { - const MEMORY_LAYOUT: Layout = Layout::List(ListLayout { - len: N, - item_layout: &T::MEMORY_LAYOUT, - }); -} - -macro_rules! impl_serialize_const_tuple { - ($($generic:ident: $generic_number:expr),*) => { - impl_serialize_const_tuple!(@impl ($($generic,)*) = $($generic: $generic_number),*); - }; - (@impl $inner:ty = $($generic:ident: $generic_number:expr),*) => { - unsafe impl<$($generic: SerializeConst),*> SerializeConst for ($($generic,)*) { - const MEMORY_LAYOUT: Layout = { - Layout::Struct(StructLayout { - size: std::mem::size_of::<($($generic,)*)>(), - data: &[ - $( - StructFieldLayout::new(std::mem::offset_of!($inner, $generic_number), $generic::MEMORY_LAYOUT), - )* - ], - }) - }; - } - }; -} - -impl_serialize_const_tuple!(T1: 0); -impl_serialize_const_tuple!(T1: 0, T2: 1); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8); -impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8, T10: 9); - -const MAX_STR_SIZE: usize = 256; - -/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time -#[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Hash)] -pub struct ConstStr { - bytes: [u8; MAX_STR_SIZE], - len: u32, -} - -#[cfg(feature = "serde")] -mod serde_bytes { - use serde::{Deserialize, Serialize, Serializer}; - - use crate::ConstStr; - - impl Serialize for ConstStr { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(self.as_str()) - } - } - - impl<'de> Deserialize<'de> for ConstStr { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Ok(ConstStr::new(&s)) - } - } -} - -unsafe impl SerializeConst for ConstStr { - const MEMORY_LAYOUT: Layout = Layout::Struct(StructLayout { - size: std::mem::size_of::(), - data: &[ - StructFieldLayout::new( - std::mem::offset_of!(Self, bytes), - Layout::List(ListLayout { - len: MAX_STR_SIZE, - item_layout: &Layout::Primitive(PrimitiveLayout { - size: std::mem::size_of::(), - }), - }), - ), - StructFieldLayout::new( - std::mem::offset_of!(Self, len), - Layout::Primitive(PrimitiveLayout { - size: std::mem::size_of::(), - }), - ), - ], - }); -} - -impl ConstStr { - /// Create a new constant string - pub const fn new(s: &str) -> Self { - let str_bytes = s.as_bytes(); - let mut bytes = [0; MAX_STR_SIZE]; - let mut i = 0; - while i < str_bytes.len() { - bytes[i] = str_bytes[i]; - i += 1; - } - Self { - bytes, - len: str_bytes.len() as u32, - } - } - - /// Get a reference to the string - pub const fn as_str(&self) -> &str { - let str_bytes = self.bytes.split_at(self.len as usize).0; - match std::str::from_utf8(str_bytes) { - Ok(s) => s, - Err(_) => panic!( - "Invalid utf8; ConstStr should only ever be constructed from valid utf8 strings" - ), - } - } - - /// Get the length of the string - pub const fn len(&self) -> usize { - self.len as usize - } - - /// Check if the string is empty - pub const fn is_empty(&self) -> bool { - self.len == 0 - } - - /// Push a character onto the string - pub const fn push(self, byte: char) -> Self { - assert!(byte.is_ascii(), "Only ASCII bytes are supported"); - let (bytes, len) = char_to_bytes(byte); - let (str, _) = bytes.split_at(len); - let Ok(str) = std::str::from_utf8(str) else { - panic!("Invalid utf8; char_to_bytes should always return valid utf8 bytes") - }; - self.push_str(str) - } - - /// Push a str onto the string - pub const fn push_str(self, str: &str) -> Self { - let Self { mut bytes, len } = self; - assert!( - str.len() + len as usize <= MAX_STR_SIZE, - "String is too long" - ); - let str_bytes = str.as_bytes(); - let new_len = len as usize + str_bytes.len(); - let mut i = 0; - while i < str_bytes.len() { - bytes[len as usize + i] = str_bytes[i]; - i += 1; - } - Self { - bytes, - len: new_len as u32, - } - } - - /// Split the string at a byte index. The byte index must be a char boundary - pub const fn split_at(self, index: usize) -> (Self, Self) { - let (left, right) = self.bytes.split_at(index); - let left = match std::str::from_utf8(left) { - Ok(s) => s, - Err(_) => { - panic!("Invalid utf8; you cannot split at a byte that is not a char boundary") - } - }; - let right = match std::str::from_utf8(right) { - Ok(s) => s, - Err(_) => { - panic!("Invalid utf8; you cannot split at a byte that is not a char boundary") - } - }; - (Self::new(left), Self::new(right)) - } - - /// Split the string at the last occurrence of a character - pub const fn rsplit_once(&self, char: char) -> Option<(Self, Self)> { - let str = self.as_str(); - let mut index = str.len() - 1; - // First find the bytes we are searching for - let (char_bytes, len) = char_to_bytes(char); - let (char_bytes, _) = char_bytes.split_at(len); - let bytes = str.as_bytes(); - - // Then walk backwards from the end of the string - loop { - let byte = bytes[index]; - // Look for char boundaries in the string and check if the bytes match - if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) { - // Split up the string into three sections: [before_char, in_char, after_char] - let (before_char, after_index) = bytes.split_at(index); - let (in_char, after_char) = after_index.split_at(char_boundary_len as usize); - if in_char.len() != char_boundary_len as usize { - panic!("in_char.len() should always be equal to char_boundary_len as usize") - } - // Check if the bytes for the current char and the target char match - let mut in_char_eq = true; - let mut i = 0; - let min_len = if in_char.len() < char_bytes.len() { - in_char.len() - } else { - char_bytes.len() - }; - while i < min_len { - in_char_eq &= in_char[i] == char_bytes[i]; - i += 1; - } - // If they do, convert the bytes to strings and return the split strings - if in_char_eq { - let Ok(before_char_str) = std::str::from_utf8(before_char) else { - panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") - }; - let Ok(after_char_str) = std::str::from_utf8(after_char) else { - panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") - }; - return Some((Self::new(before_char_str), Self::new(after_char_str))); - } - } - match index.checked_sub(1) { - Some(new_index) => index = new_index, - None => return None, - } - } - } - - /// Split the string at the first occurrence of a character - pub const fn split_once(&self, char: char) -> Option<(Self, Self)> { - let str = self.as_str(); - let mut index = 0; - // First find the bytes we are searching for - let (char_bytes, len) = char_to_bytes(char); - let (char_bytes, _) = char_bytes.split_at(len); - let bytes = str.as_bytes(); - - // Then walk forwards from the start of the string - while index < bytes.len() { - let byte = bytes[index]; - // Look for char boundaries in the string and check if the bytes match - if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) { - // Split up the string into three sections: [before_char, in_char, after_char] - let (before_char, after_index) = bytes.split_at(index); - let (in_char, after_char) = after_index.split_at(char_boundary_len as usize); - if in_char.len() != char_boundary_len as usize { - panic!("in_char.len() should always be equal to char_boundary_len as usize") - } - // Check if the bytes for the current char and the target char match - let mut in_char_eq = true; - let mut i = 0; - let min_len = if in_char.len() < char_bytes.len() { - in_char.len() - } else { - char_bytes.len() - }; - while i < min_len { - in_char_eq &= in_char[i] == char_bytes[i]; - i += 1; - } - // If they do, convert the bytes to strings and return the split strings - if in_char_eq { - let Ok(before_char_str) = std::str::from_utf8(before_char) else { - panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") - }; - let Ok(after_char_str) = std::str::from_utf8(after_char) else { - panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") - }; - return Some((Self::new(before_char_str), Self::new(after_char_str))); - } - } - index += 1 - } - None - } -} - -impl std::fmt::Debug for ConstStr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.as_str()) - } -} - -#[test] -fn test_rsplit_once() { - let str = ConstStr::new("hello world"); - assert_eq!( - str.rsplit_once(' '), - Some((ConstStr::new("hello"), ConstStr::new("world"))) - ); - - let unicode_str = ConstStr::new("hiπŸ˜€helloπŸ˜€worldπŸ˜€world"); - assert_eq!( - unicode_str.rsplit_once('πŸ˜€'), - Some((ConstStr::new("hiπŸ˜€helloπŸ˜€world"), ConstStr::new("world"))) - ); - assert_eq!(unicode_str.rsplit_once('❌'), None); - - for _ in 0..100 { - let random_str: String = (0..rand::random::() % 50) - .map(|_| rand::random::()) - .collect(); - let konst = ConstStr::new(&random_str); - let mut seen_chars = std::collections::HashSet::new(); - for char in random_str.chars().rev() { - let (char_bytes, len) = char_to_bytes(char); - let char_bytes = &char_bytes[..len]; - assert_eq!(char_bytes, char.to_string().as_bytes()); - if seen_chars.contains(&char) { - continue; - } - seen_chars.insert(char); - let (correct_left, correct_right) = random_str.rsplit_once(char).unwrap(); - let (left, right) = konst.rsplit_once(char).unwrap(); - println!("splitting {random_str:?} at {char:?}"); - assert_eq!(left.as_str(), correct_left); - assert_eq!(right.as_str(), correct_right); - } - } -} - -const CONTINUED_CHAR_MASK: u8 = 0b10000000; -const BYTE_CHAR_BOUNDARIES: [u8; 4] = [0b00000000, 0b11000000, 0b11100000, 0b11110000]; - -// Const version of https://doc.rust-lang.org/src/core/char/methods.rs.html#1765-1797 -const fn char_to_bytes(char: char) -> ([u8; 4], usize) { - let code = char as u32; - let len = char.len_utf8(); - let mut bytes = [0; 4]; - match len { - 1 => { - bytes[0] = code as u8; - } - 2 => { - bytes[0] = ((code >> 6) & 0x1F) as u8 | BYTE_CHAR_BOUNDARIES[1]; - bytes[1] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK; - } - 3 => { - bytes[0] = ((code >> 12) & 0x0F) as u8 | BYTE_CHAR_BOUNDARIES[2]; - bytes[1] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK; - bytes[2] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK; - } - 4 => { - bytes[0] = ((code >> 18) & 0x07) as u8 | BYTE_CHAR_BOUNDARIES[3]; - bytes[1] = ((code >> 12) & 0x3F) as u8 | CONTINUED_CHAR_MASK; - bytes[2] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK; - bytes[3] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK; - } - _ => panic!( - "encode_utf8: need more than 4 bytes to encode the unicode character, but the buffer has 4 bytes" - ), - }; - (bytes, len) -} - -#[test] -fn fuzz_char_to_bytes() { - use std::char; - for _ in 0..100 { - let char = rand::random::(); - let (bytes, len) = char_to_bytes(char); - let str = std::str::from_utf8(&bytes[..len]).unwrap(); - assert_eq!(char.to_string(), str); - } -} - -const fn utf8_char_boundary_to_char_len(byte: u8) -> Option { - match byte { - 0b00000000..=0b01111111 => Some(1), - 0b11000000..=0b11011111 => Some(2), - 0b11100000..=0b11101111 => Some(3), - 0b11110000..=0b11111111 => Some(4), - _ => None, - } -} - -#[test] -fn fuzz_utf8_byte_to_char_len() { - for _ in 0..100 { - let random_string: String = (0..rand::random::()) - .map(|_| rand::random::()) - .collect(); - let bytes = random_string.as_bytes(); - let chars: std::collections::HashMap<_, _> = random_string.char_indices().collect(); - for (i, byte) in bytes.iter().enumerate() { - match utf8_char_boundary_to_char_len(*byte) { - Some(char_len) => { - let char = chars - .get(&i) - .unwrap_or_else(|| panic!("{byte:b} is not a character boundary")); - assert_eq!(char.len_utf8(), char_len as usize); - } - None => { - assert!(!chars.contains_key(&i), "{byte:b} is a character boundary"); - } - } - } - } -} - -/// Serialize a struct that is stored at the pointer passed in -const fn serialize_const_struct( - ptr: *const (), - mut to: ConstVec, - layout: &StructLayout, -) -> ConstVec { - let mut i = 0; - while i < layout.data.len() { - // Serialize the field at the offset pointer in the struct - let StructFieldLayout { offset, layout } = &layout.data[i]; - let field = ptr.wrapping_byte_add(*offset as _); - to = serialize_const_ptr(field, to, layout); - i += 1; - } - to -} - -/// Serialize an enum that is stored at the pointer passed in -const fn serialize_const_enum( - ptr: *const (), - mut to: ConstVec, - layout: &EnumLayout, -) -> ConstVec { - let mut discriminant = 0; - - let byte_ptr = ptr as *const u8; - let mut offset = 0; - while offset < layout.discriminant.size { - // If the bytes are reversed, walk backwards from the end of the number when pushing bytes - let byte = if cfg!(target_endian = "big") { - unsafe { - byte_ptr - .wrapping_byte_add((layout.discriminant.size - offset - 1) as _) - .read() - } - } else { - unsafe { byte_ptr.wrapping_byte_add(offset as _).read() } - }; - to = to.push(byte); - discriminant |= (byte as u32) << (offset * 8); - offset += 1; - } - - let mut i = 0; - while i < layout.variants.len() { - // If the variant is the discriminated one, serialize it - let EnumVariant { tag, data, .. } = &layout.variants[i]; - if discriminant == *tag { - let data_ptr = ptr.wrapping_byte_offset(layout.variants_offset as _); - to = serialize_const_struct(data_ptr, to, data); - break; - } - i += 1; - } - to -} - -/// Serialize a primitive type that is stored at the pointer passed in -const fn serialize_const_primitive( - ptr: *const (), - mut to: ConstVec, - layout: &PrimitiveLayout, -) -> ConstVec { - let ptr = ptr as *const u8; - let mut offset = 0; - while offset < layout.size { - // If the bytes are reversed, walk backwards from the end of the number when pushing bytes - if cfg!(any(target_endian = "big", feature = "test-big-endian")) { - to = to.push(unsafe { - ptr.wrapping_byte_offset((layout.size - offset - 1) as _) - .read() - }); - } else { - to = to.push(unsafe { ptr.wrapping_byte_offset(offset as _).read() }); - } - offset += 1; - } - to -} - -/// Serialize a constant sized array that is stored at the pointer passed in -const fn serialize_const_list( +/// Serialize a pointer to a type that is stored at the pointer passed in +const unsafe fn serialize_const_ptr( ptr: *const (), - mut to: ConstVec, - layout: &ListLayout, + to: ConstVec, + layout: &Layout, ) -> ConstVec { - let len = layout.len; - let mut i = 0; - while i < len { - let field = ptr.wrapping_byte_offset((i * layout.item_layout.size()) as _); - to = serialize_const_ptr(field, to, layout.item_layout); - i += 1; - } - to -} - -/// Serialize a pointer to a type that is stored at the pointer passed in -const fn serialize_const_ptr(ptr: *const (), to: ConstVec, layout: &Layout) -> ConstVec { match layout { Layout::Enum(layout) => serialize_const_enum(ptr, to, layout), Layout::Struct(layout) => serialize_const_struct(ptr, to, layout), + Layout::Array(layout) => serialize_const_array(ptr, to, layout), Layout::List(layout) => serialize_const_list(ptr, to, layout), Layout::Primitive(layout) => serialize_const_primitive(ptr, to, layout), } @@ -710,156 +100,31 @@ const fn serialize_const_ptr(ptr: *const (), to: ConstVec, layout: &Layout) /// b: 0x22, /// c: 0x33333333, /// }, buffer); -/// let buf = buffer.read(); -/// assert_eq!(buf.as_ref(), &[0x11, 0x11, 0x11, 0x11, 0x22, 0x33, 0x33, 0x33, 0x33]); +/// assert_eq!(buffer.as_ref(), &[0xa3, 0x61, 0x61, 0x1a, 0x11, 0x11, 0x11, 0x11, 0x61, 0x62, 0x18, 0x22, 0x61, 0x63, 0x1a, 0x33, 0x33, 0x33, 0x33]); /// ``` #[must_use = "The data is serialized into the returned buffer"] pub const fn serialize_const(data: &T, to: ConstVec) -> ConstVec { let ptr = data as *const T as *const (); - serialize_const_ptr(ptr, to, &T::MEMORY_LAYOUT) -} - -/// Deserialize a primitive type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. -const fn deserialize_const_primitive<'a, const N: usize>( - mut from: ConstReadBuffer<'a>, - layout: &PrimitiveLayout, - out: (usize, [MaybeUninit; N]), -) -> Option<(ConstReadBuffer<'a>, [MaybeUninit; N])> { - let (start, mut out) = out; - let mut offset = 0; - while offset < layout.size { - // If the bytes are reversed, walk backwards from the end of the number when filling in bytes - let (from_new, value) = match from.get() { - Some(data) => data, - None => return None, - }; - from = from_new; - if cfg!(any(target_endian = "big", feature = "test-big-endian")) { - out[start + layout.size - offset - 1] = MaybeUninit::new(value); - } else { - out[start + offset] = MaybeUninit::new(value); - } - offset += 1; - } - Some((from, out)) -} - -/// Deserialize a struct type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. -const fn deserialize_const_struct<'a, const N: usize>( - mut from: ConstReadBuffer<'a>, - layout: &StructLayout, - out: (usize, [MaybeUninit; N]), -) -> Option<(ConstReadBuffer<'a>, [MaybeUninit; N])> { - let (start, mut out) = out; - let mut i = 0; - while i < layout.data.len() { - // Deserialize the field at the offset pointer in the struct - let StructFieldLayout { offset, layout } = &layout.data[i]; - let (new_from, new_out) = match deserialize_const_ptr(from, layout, (start + *offset, out)) - { - Some(data) => data, - None => return None, - }; - from = new_from; - out = new_out; - i += 1; - } - Some((from, out)) -} - -/// Deserialize an enum type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. -const fn deserialize_const_enum<'a, const N: usize>( - mut from: ConstReadBuffer<'a>, - layout: &EnumLayout, - out: (usize, [MaybeUninit; N]), -) -> Option<(ConstReadBuffer<'a>, [MaybeUninit; N])> { - let (start, mut out) = out; - let mut discriminant = 0; - - // First, deserialize the discriminant - let mut offset = 0; - while offset < layout.discriminant.size { - // If the bytes are reversed, walk backwards from the end of the number when filling in bytes - let (from_new, value) = match from.get() { - Some(data) => data, - None => return None, - }; - from = from_new; - if cfg!(target_endian = "big") { - out[start + layout.size - offset - 1] = MaybeUninit::new(value); - discriminant |= (value as u32) << ((layout.discriminant.size - offset - 1) * 8); - } else { - out[start + offset] = MaybeUninit::new(value); - discriminant |= (value as u32) << (offset * 8); - } - offset += 1; - } - - // Then, deserialize the variant - let mut i = 0; - let mut matched_variant = false; - while i < layout.variants.len() { - // If the variant is the discriminated one, deserialize it - let EnumVariant { tag, data, .. } = &layout.variants[i]; - if discriminant == *tag { - let offset = layout.variants_offset; - let (new_from, new_out) = - match deserialize_const_struct(from, data, (start + offset, out)) { - Some(data) => data, - None => return None, - }; - from = new_from; - out = new_out; - matched_variant = true; - break; - } - i += 1; - } - if !matched_variant { - return None; - } - - Some((from, out)) -} - -/// Deserialize a list type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. -const fn deserialize_const_list<'a, const N: usize>( - mut from: ConstReadBuffer<'a>, - layout: &ListLayout, - out: (usize, [MaybeUninit; N]), -) -> Option<(ConstReadBuffer<'a>, [MaybeUninit; N])> { - let (start, mut out) = out; - let len = layout.len; - let item_layout = layout.item_layout; - let mut i = 0; - while i < len { - let (new_from, new_out) = - match deserialize_const_ptr(from, item_layout, (start + i * item_layout.size(), out)) { - Some(data) => data, - None => return None, - }; - from = new_from; - out = new_out; - i += 1; - } - Some((from, out)) + // SAFETY: The pointer is valid and the layout is correct + unsafe { serialize_const_ptr(ptr, to, &T::MEMORY_LAYOUT) } } /// Deserialize a type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. -const fn deserialize_const_ptr<'a, const N: usize>( - from: ConstReadBuffer<'a>, +const fn deserialize_const_ptr<'a>( + from: &'a [u8], layout: &Layout, - out: (usize, [MaybeUninit; N]), -) -> Option<(ConstReadBuffer<'a>, [MaybeUninit; N])> { + out: &mut [MaybeUninit], +) -> Option<&'a [u8]> { match layout { Layout::Enum(layout) => deserialize_const_enum(from, layout, out), Layout::Struct(layout) => deserialize_const_struct(from, layout, out), + Layout::Array(layout) => deserialize_const_array(from, layout, out), Layout::List(layout) => deserialize_const_list(from, layout, out), Layout::Primitive(layout) => deserialize_const_primitive(from, layout, out), } } -/// Deserialize a type into the output buffer. Accepts `(type, ConstVec)` as input and returns `Option<(ConstReadBuffer, Instance of type)>` +/// Deserialize a type into the output buffer. Accepts `(type, ConstVec)` as input and returns `Option<(&'a [u8], Instance of type)>` /// /// # Example /// ```rust @@ -879,7 +144,7 @@ const fn deserialize_const_ptr<'a, const N: usize>( /// c: 0x33333333, /// d: 0x44444444, /// }, buffer); -/// let buf = buffer.read(); +/// let buf = buffer.as_ref(); /// assert_eq!(deserialize_const!(Struct, buf).unwrap().1, Struct { /// a: 0x11111111, /// b: 0x22, @@ -902,14 +167,13 @@ macro_rules! deserialize_const { /// N must be `std::mem::size_of::()` #[must_use = "The data is deserialized from the input buffer"] pub const unsafe fn deserialize_const_raw( - from: ConstReadBuffer, -) -> Option<(ConstReadBuffer, T)> { + from: &[u8], +) -> Option<(&[u8], T)> { // Create uninitized memory with the size of the type - let out = [MaybeUninit::uninit(); N]; + let mut out = [MaybeUninit::uninit(); N]; // Fill in the bytes into the buffer for the type - let (from, out) = match deserialize_const_ptr(from, &T::MEMORY_LAYOUT, (0, out)) { - Some(data) => data, - None => return None, + let Some(from) = deserialize_const_ptr(from, &T::MEMORY_LAYOUT, &mut out) else { + return None; }; // Now that the memory is filled in, transmute it into the type Some((from, unsafe { diff --git a/packages/const-serialize/src/list.rs b/packages/const-serialize/src/list.rs new file mode 100644 index 0000000000..1b94a2100b --- /dev/null +++ b/packages/const-serialize/src/list.rs @@ -0,0 +1,119 @@ +use crate::*; + +/// The layout for a dynamically sized list. The list layout is just a length and an item layout. +#[derive(Debug, Copy, Clone)] +pub struct ListLayout { + /// The size of the struct backing the list + pub(crate) size: usize, + /// The byte offset of the length field + len_offset: usize, + /// The layout of the length field + len_layout: PrimitiveLayout, + /// The byte offset of the data field + data_offset: usize, + /// The layout of the data field + data_layout: ArrayLayout, +} + +impl ListLayout { + /// Create a new list layout + pub const fn new( + size: usize, + len_offset: usize, + len_layout: PrimitiveLayout, + data_offset: usize, + data_layout: ArrayLayout, + ) -> Self { + Self { + size, + len_offset, + len_layout, + data_offset, + data_layout, + } + } +} + +/// Serialize a dynamically sized list that is stored at the pointer passed in +pub(crate) const unsafe fn serialize_const_list( + ptr: *const (), + mut to: ConstVec, + layout: &ListLayout, +) -> ConstVec { + // Read the length of the list + let len_ptr = ptr.wrapping_byte_offset(layout.len_offset as _); + let len = layout.len_layout.read(len_ptr as *const u8) as usize; + + let data_ptr = ptr.wrapping_byte_offset(layout.data_offset as _); + let item_layout = layout.data_layout.item_layout; + // If the item size is 1, deserialize as bytes directly + if item_layout.size() == 1 { + let slice = std::slice::from_raw_parts(data_ptr as *const u8, len); + to = write_bytes(to, slice); + } + // Otherwise, deserialize as a list of items + else { + let mut i = 0; + to = write_array(to, len); + while i < len { + let item = data_ptr.wrapping_byte_offset((i * item_layout.size()) as _); + to = serialize_const_ptr(item, to, item_layout); + i += 1; + } + } + to +} + +/// Deserialize a list type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. +pub(crate) const fn deserialize_const_list<'a>( + from: &'a [u8], + layout: &ListLayout, + out: &mut [MaybeUninit], +) -> Option<&'a [u8]> { + let Some((_, len_out)) = out.split_at_mut_checked(layout.len_offset) else { + return None; + }; + + // If the list items are only one byte, serialize as bytes directly + let item_layout = layout.data_layout.item_layout; + if item_layout.size() == 1 { + let Ok((bytes, new_from)) = take_bytes(from) else { + return None; + }; + // Write out the length of the list + layout.len_layout.write(bytes.len() as u32, len_out); + let Some((_, data_out)) = out.split_at_mut_checked(layout.data_offset) else { + return None; + }; + let mut offset = 0; + while offset < bytes.len() { + data_out[offset] = MaybeUninit::new(bytes[offset]); + offset += 1; + } + Some(new_from) + } + // Otherwise, serialize as an list of objects + else { + let Ok((len, mut from)) = take_array(from) else { + return None; + }; + // Write out the length of the list + layout.len_layout.write(len as u32, len_out); + let Some((_, mut data_out)) = out.split_at_mut_checked(layout.data_offset) else { + return None; + }; + let mut i = 0; + while i < len { + let Some(new_from) = deserialize_const_ptr(from, item_layout, data_out) else { + return None; + }; + let Some((_, item_out)) = data_out.split_at_mut_checked(item_layout.size()) else { + return None; + }; + data_out = item_out; + from = new_from; + i += 1; + } + Some(from) + } +} diff --git a/packages/const-serialize/src/primitive.rs b/packages/const-serialize/src/primitive.rs new file mode 100644 index 0000000000..0c511c3887 --- /dev/null +++ b/packages/const-serialize/src/primitive.rs @@ -0,0 +1,121 @@ +use crate::*; +use std::mem::MaybeUninit; + +/// The layout for a primitive type. The bytes will be reversed if the target is big endian. +#[derive(Debug, Copy, Clone)] +pub struct PrimitiveLayout { + pub(crate) size: usize, +} + +impl PrimitiveLayout { + /// Create a new primitive layout + pub const fn new(size: usize) -> Self { + Self { size } + } + + /// Read the value from the given pointer + /// + /// # Safety + /// The pointer must be valid for reads of `self.size` bytes. + pub const unsafe fn read(self, byte_ptr: *const u8) -> u32 { + let mut value = 0; + let mut offset = 0; + while offset < self.size { + // If the bytes are reversed, walk backwards from the end of the number when pushing bytes + let byte = if cfg!(target_endian = "big") { + unsafe { + byte_ptr + .wrapping_byte_add((self.size - offset - 1) as _) + .read() + } + } else { + unsafe { byte_ptr.wrapping_byte_add(offset as _).read() } + }; + value |= (byte as u32) << (offset * 8); + offset += 1; + } + value + } + + /// Write the value to the given buffer + pub const fn write(self, value: u32, out: &mut [MaybeUninit]) { + let bytes = value.to_ne_bytes(); + let mut offset = 0; + while offset < self.size { + out[offset] = MaybeUninit::new(bytes[offset]); + offset += 1; + } + } +} + +macro_rules! impl_serialize_const { + ($type:ty) => { + unsafe impl SerializeConst for $type { + const MEMORY_LAYOUT: Layout = Layout::Primitive(PrimitiveLayout { + size: std::mem::size_of::<$type>(), + }); + } + }; +} + +impl_serialize_const!(u8); +impl_serialize_const!(u16); +impl_serialize_const!(u32); +impl_serialize_const!(u64); +impl_serialize_const!(i8); +impl_serialize_const!(i16); +impl_serialize_const!(i32); +impl_serialize_const!(i64); +impl_serialize_const!(bool); +impl_serialize_const!(f32); +impl_serialize_const!(f64); + +/// Serialize a primitive type that is stored at the pointer passed in +pub(crate) const unsafe fn serialize_const_primitive( + ptr: *const (), + to: ConstVec, + layout: &PrimitiveLayout, +) -> ConstVec { + let ptr = ptr as *const u8; + let mut offset = 0; + let mut i64_bytes = [0u8; 8]; + while offset < layout.size { + // If the bytes are reversed, walk backwards from the end of the number when pushing bytes + let byte = unsafe { + if cfg!(any(target_endian = "big", feature = "test-big-endian")) { + ptr.wrapping_byte_offset((layout.size - offset - 1) as _) + .read() + } else { + ptr.wrapping_byte_offset(offset as _).read() + } + }; + i64_bytes[offset] = byte; + offset += 1; + } + let number = i64::from_ne_bytes(i64_bytes); + write_number(to, number) +} + +/// Deserialize a primitive type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. +pub(crate) const fn deserialize_const_primitive<'a>( + from: &'a [u8], + layout: &PrimitiveLayout, + out: &mut [MaybeUninit], +) -> Option<&'a [u8]> { + let mut offset = 0; + let Ok((number, from)) = take_number(from) else { + return None; + }; + let bytes = number.to_le_bytes(); + while offset < layout.size { + // If the bytes are reversed, walk backwards from the end of the number when filling in bytes + let byte = bytes[offset]; + if cfg!(any(target_endian = "big", feature = "test-big-endian")) { + out[layout.size - offset - 1] = MaybeUninit::new(byte); + } else { + out[offset] = MaybeUninit::new(byte); + } + offset += 1; + } + Some(from) +} diff --git a/packages/const-serialize/src/str.rs b/packages/const-serialize/src/str.rs new file mode 100644 index 0000000000..f838e23505 --- /dev/null +++ b/packages/const-serialize/src/str.rs @@ -0,0 +1,391 @@ +use crate::*; +use std::{char, hash::Hash, mem::MaybeUninit}; + +const MAX_STR_SIZE: usize = 256; + +/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time +#[derive(Clone, Copy, Debug)] +pub struct ConstStr { + pub(crate) bytes: [MaybeUninit; MAX_STR_SIZE], + pub(crate) len: u32, +} + +#[cfg(feature = "serde")] +mod serde_bytes { + use serde::{Deserialize, Serialize, Serializer}; + + use crate::ConstStr; + + impl Serialize for ConstStr { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } + } + + impl<'de> Deserialize<'de> for ConstStr { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(ConstStr::new(&s)) + } + } +} + +unsafe impl SerializeConst for ConstStr { + const MEMORY_LAYOUT: Layout = Layout::List(ListLayout::new( + std::mem::size_of::(), + std::mem::offset_of!(Self, len), + PrimitiveLayout { + size: std::mem::size_of::(), + }, + std::mem::offset_of!(Self, bytes), + ArrayLayout { + len: MAX_STR_SIZE, + item_layout: &Layout::Primitive(PrimitiveLayout { + size: std::mem::size_of::(), + }), + }, + )); +} + +impl ConstStr { + /// Create a new constant string + pub const fn new(s: &str) -> Self { + let str_bytes = s.as_bytes(); + let mut bytes = [MaybeUninit::uninit(); MAX_STR_SIZE]; + let mut i = 0; + while i < str_bytes.len() { + bytes[i] = MaybeUninit::new(str_bytes[i]); + i += 1; + } + Self { + bytes, + len: str_bytes.len() as u32, + } + } + + /// Get the bytes of the initialized portion of the string + const fn bytes(&self) -> &[u8] { + // Safety: All bytes up to the pointer are initialized + unsafe { + &*(self.bytes.split_at(self.len as usize).0 as *const [MaybeUninit] + as *const [u8]) + } + } + + /// Get a reference to the string + pub const fn as_str(&self) -> &str { + let str_bytes = self.bytes(); + match std::str::from_utf8(str_bytes) { + Ok(s) => s, + Err(_) => panic!( + "Invalid utf8; ConstStr should only ever be constructed from valid utf8 strings" + ), + } + } + + /// Get the length of the string + pub const fn len(&self) -> usize { + self.len as usize + } + + /// Check if the string is empty + pub const fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Push a character onto the string + pub const fn push(self, byte: char) -> Self { + assert!(byte.is_ascii(), "Only ASCII bytes are supported"); + let (bytes, len) = char_to_bytes(byte); + let (str, _) = bytes.split_at(len); + let Ok(str) = std::str::from_utf8(str) else { + panic!("Invalid utf8; char_to_bytes should always return valid utf8 bytes") + }; + self.push_str(str) + } + + /// Push a str onto the string + pub const fn push_str(self, str: &str) -> Self { + let Self { mut bytes, len } = self; + assert!( + str.len() + len as usize <= MAX_STR_SIZE, + "String is too long" + ); + let str_bytes = str.as_bytes(); + let new_len = len as usize + str_bytes.len(); + let mut i = 0; + while i < str_bytes.len() { + bytes[len as usize + i] = MaybeUninit::new(str_bytes[i]); + i += 1; + } + Self { + bytes, + len: new_len as u32, + } + } + + /// Split the string at a byte index. The byte index must be a char boundary + pub const fn split_at(self, index: usize) -> (Self, Self) { + let (left, right) = self.bytes().split_at(index); + let left = match std::str::from_utf8(left) { + Ok(s) => s, + Err(_) => { + panic!("Invalid utf8; you cannot split at a byte that is not a char boundary") + } + }; + let right = match std::str::from_utf8(right) { + Ok(s) => s, + Err(_) => { + panic!("Invalid utf8; you cannot split at a byte that is not a char boundary") + } + }; + (Self::new(left), Self::new(right)) + } + + /// Split the string at the last occurrence of a character + pub const fn rsplit_once(&self, char: char) -> Option<(Self, Self)> { + let str = self.as_str(); + let mut index = str.len() - 1; + // First find the bytes we are searching for + let (char_bytes, len) = char_to_bytes(char); + let (char_bytes, _) = char_bytes.split_at(len); + let bytes = str.as_bytes(); + + // Then walk backwards from the end of the string + loop { + let byte = bytes[index]; + // Look for char boundaries in the string and check if the bytes match + if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) { + // Split up the string into three sections: [before_char, in_char, after_char] + let (before_char, after_index) = bytes.split_at(index); + let (in_char, after_char) = after_index.split_at(char_boundary_len as usize); + if in_char.len() != char_boundary_len as usize { + panic!("in_char.len() should always be equal to char_boundary_len as usize") + } + // Check if the bytes for the current char and the target char match + let mut in_char_eq = true; + let mut i = 0; + let min_len = if in_char.len() < char_bytes.len() { + in_char.len() + } else { + char_bytes.len() + }; + while i < min_len { + in_char_eq &= in_char[i] == char_bytes[i]; + i += 1; + } + // If they do, convert the bytes to strings and return the split strings + if in_char_eq { + let Ok(before_char_str) = std::str::from_utf8(before_char) else { + panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") + }; + let Ok(after_char_str) = std::str::from_utf8(after_char) else { + panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") + }; + return Some((Self::new(before_char_str), Self::new(after_char_str))); + } + } + match index.checked_sub(1) { + Some(new_index) => index = new_index, + None => return None, + } + } + } + + /// Split the string at the first occurrence of a character + pub const fn split_once(&self, char: char) -> Option<(Self, Self)> { + let str = self.as_str(); + let mut index = 0; + // First find the bytes we are searching for + let (char_bytes, len) = char_to_bytes(char); + let (char_bytes, _) = char_bytes.split_at(len); + let bytes = str.as_bytes(); + + // Then walk forwards from the start of the string + while index < bytes.len() { + let byte = bytes[index]; + // Look for char boundaries in the string and check if the bytes match + if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) { + // Split up the string into three sections: [before_char, in_char, after_char] + let (before_char, after_index) = bytes.split_at(index); + let (in_char, after_char) = after_index.split_at(char_boundary_len as usize); + if in_char.len() != char_boundary_len as usize { + panic!("in_char.len() should always be equal to char_boundary_len as usize") + } + // Check if the bytes for the current char and the target char match + let mut in_char_eq = true; + let mut i = 0; + let min_len = if in_char.len() < char_bytes.len() { + in_char.len() + } else { + char_bytes.len() + }; + while i < min_len { + in_char_eq &= in_char[i] == char_bytes[i]; + i += 1; + } + // If they do, convert the bytes to strings and return the split strings + if in_char_eq { + let Ok(before_char_str) = std::str::from_utf8(before_char) else { + panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") + }; + let Ok(after_char_str) = std::str::from_utf8(after_char) else { + panic!("Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary") + }; + return Some((Self::new(before_char_str), Self::new(after_char_str))); + } + } + index += 1 + } + None + } +} + +impl PartialEq for ConstStr { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} + +impl Eq for ConstStr {} + +impl PartialOrd for ConstStr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ConstStr { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl Hash for ConstStr { + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } +} + +#[test] +fn test_rsplit_once() { + let str = ConstStr::new("hello world"); + assert_eq!( + str.rsplit_once(' '), + Some((ConstStr::new("hello"), ConstStr::new("world"))) + ); + + let unicode_str = ConstStr::new("hiπŸ˜€helloπŸ˜€worldπŸ˜€world"); + assert_eq!( + unicode_str.rsplit_once('πŸ˜€'), + Some((ConstStr::new("hiπŸ˜€helloπŸ˜€world"), ConstStr::new("world"))) + ); + assert_eq!(unicode_str.rsplit_once('❌'), None); + + for _ in 0..100 { + let random_str: String = (0..rand::random::() % 50) + .map(|_| rand::random::()) + .collect(); + let konst = ConstStr::new(&random_str); + let mut seen_chars = std::collections::HashSet::new(); + for char in random_str.chars().rev() { + let (char_bytes, len) = char_to_bytes(char); + let char_bytes = &char_bytes[..len]; + assert_eq!(char_bytes, char.to_string().as_bytes()); + if seen_chars.contains(&char) { + continue; + } + seen_chars.insert(char); + let (correct_left, correct_right) = random_str.rsplit_once(char).unwrap(); + let (left, right) = konst.rsplit_once(char).unwrap(); + println!("splitting {random_str:?} at {char:?}"); + assert_eq!(left.as_str(), correct_left); + assert_eq!(right.as_str(), correct_right); + } + } +} + +const CONTINUED_CHAR_MASK: u8 = 0b10000000; +const BYTE_CHAR_BOUNDARIES: [u8; 4] = [0b00000000, 0b11000000, 0b11100000, 0b11110000]; + +// Const version of https://doc.rust-lang.org/src/core/char/methods.rs.html#1765-1797 +const fn char_to_bytes(char: char) -> ([u8; 4], usize) { + let code = char as u32; + let len = char.len_utf8(); + let mut bytes = [0; 4]; + match len { + 1 => { + bytes[0] = code as u8; + } + 2 => { + bytes[0] = ((code >> 6) & 0x1F) as u8 | BYTE_CHAR_BOUNDARIES[1]; + bytes[1] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK; + } + 3 => { + bytes[0] = ((code >> 12) & 0x0F) as u8 | BYTE_CHAR_BOUNDARIES[2]; + bytes[1] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK; + bytes[2] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK; + } + 4 => { + bytes[0] = ((code >> 18) & 0x07) as u8 | BYTE_CHAR_BOUNDARIES[3]; + bytes[1] = ((code >> 12) & 0x3F) as u8 | CONTINUED_CHAR_MASK; + bytes[2] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK; + bytes[3] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK; + } + _ => panic!( + "encode_utf8: need more than 4 bytes to encode the unicode character, but the buffer has 4 bytes" + ), + }; + (bytes, len) +} + +#[test] +fn fuzz_char_to_bytes() { + use std::char; + for _ in 0..100 { + let char = rand::random::(); + let (bytes, len) = char_to_bytes(char); + let str = std::str::from_utf8(&bytes[..len]).unwrap(); + assert_eq!(char.to_string(), str); + } +} + +const fn utf8_char_boundary_to_char_len(byte: u8) -> Option { + match byte { + 0b00000000..=0b01111111 => Some(1), + 0b11000000..=0b11011111 => Some(2), + 0b11100000..=0b11101111 => Some(3), + 0b11110000..=0b11111111 => Some(4), + _ => None, + } +} + +#[test] +fn fuzz_utf8_byte_to_char_len() { + for _ in 0..100 { + let random_string: String = (0..rand::random::()) + .map(|_| rand::random::()) + .collect(); + let bytes = random_string.as_bytes(); + let chars: std::collections::HashMap<_, _> = random_string.char_indices().collect(); + for (i, byte) in bytes.iter().enumerate() { + match utf8_char_boundary_to_char_len(*byte) { + Some(char_len) => { + let char = chars + .get(&i) + .unwrap_or_else(|| panic!("{byte:b} is not a character boundary")); + assert_eq!(char.len_utf8(), char_len as usize); + } + None => { + assert!(!chars.contains_key(&i), "{byte:b} is a character boundary"); + } + } + } + } +} diff --git a/packages/const-serialize/src/struct.rs b/packages/const-serialize/src/struct.rs new file mode 100644 index 0000000000..a2db822b6a --- /dev/null +++ b/packages/const-serialize/src/struct.rs @@ -0,0 +1,120 @@ +use crate::*; + +/// Plain old data for a field. Stores the offset of the field in the struct and the layout of the field. +#[derive(Debug, Copy, Clone)] +pub struct StructFieldLayout { + name: &'static str, + offset: usize, + layout: Layout, +} + +impl StructFieldLayout { + /// Create a new struct field layout + pub const fn new(name: &'static str, offset: usize, layout: Layout) -> Self { + Self { + name, + offset, + layout, + } + } +} + +/// Layout for a struct. The struct layout is just a list of fields with offsets +#[derive(Debug, Copy, Clone)] +pub struct StructLayout { + pub(crate) size: usize, + pub(crate) data: &'static [StructFieldLayout], +} + +impl StructLayout { + /// Create a new struct layout + pub const fn new(size: usize, data: &'static [StructFieldLayout]) -> Self { + Self { size, data } + } +} + +/// Serialize a struct that is stored at the pointer passed in +pub(crate) const unsafe fn serialize_const_struct( + ptr: *const (), + to: ConstVec, + layout: &StructLayout, +) -> ConstVec { + let mut i = 0; + let field_count = layout.data.len(); + let mut to = write_map(to, field_count); + while i < field_count { + // Serialize the field at the offset pointer in the struct + let StructFieldLayout { + name, + offset, + layout, + } = &layout.data[i]; + to = write_map_key(to, name); + let field = ptr.wrapping_byte_add(*offset as _); + to = serialize_const_ptr(field, to, layout); + i += 1; + } + to +} + +/// Deserialize a struct type into the out buffer at the offset passed in. Returns a new version of the buffer with the data added. +pub(crate) const fn deserialize_const_struct<'a>( + from: &'a [u8], + layout: &StructLayout, + out: &mut [MaybeUninit], +) -> Option<&'a [u8]> { + let Ok((map, from)) = take_map(from) else { + return None; + }; + let mut i = 0; + while i < layout.data.len() { + // Deserialize the field at the offset pointer in the struct + let StructFieldLayout { + name, + offset, + layout, + } = &layout.data[i]; + let Ok(Some(from)) = map.find(name) else { + return None; + }; + let Some((_, field_bytes)) = out.split_at_mut_checked(*offset) else { + return None; + }; + if deserialize_const_ptr(from, layout, field_bytes).is_none() { + return None; + } + i += 1; + } + Some(from) +} + +macro_rules! impl_serialize_const_tuple { + ($($generic:ident: $generic_number:expr),*) => { + impl_serialize_const_tuple!(@impl ($($generic,)*) = $($generic: $generic_number),*); + }; + (@impl $inner:ty = $($generic:ident: $generic_number:expr),*) => { + unsafe impl<$($generic: SerializeConst),*> SerializeConst for ($($generic,)*) { + const MEMORY_LAYOUT: Layout = { + Layout::Struct(StructLayout { + size: std::mem::size_of::<($($generic,)*)>(), + data: &[ + $( + StructFieldLayout::new(stringify!($generic_number), std::mem::offset_of!($inner, $generic_number), $generic::MEMORY_LAYOUT), + )* + ], + }) + }; + } + }; +} + +impl_serialize_const_tuple!(T1: 0); +impl_serialize_const_tuple!(T1: 0, T2: 1); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8); +impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8: 7, T9: 8, T10: 9); diff --git a/packages/const-serialize/tests/enum.rs b/packages/const-serialize/tests/enum.rs index a0df9f160c..5b8e286ebd 100644 --- a/packages/const-serialize/tests/enum.rs +++ b/packages/const-serialize/tests/enum.rs @@ -81,7 +81,7 @@ fn test_serialize_enum() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); let data = Enum::B { @@ -91,7 +91,7 @@ fn test_serialize_enum() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); } @@ -110,7 +110,7 @@ fn test_serialize_list_of_lopsided_enums() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data); let data = [ @@ -126,7 +126,7 @@ fn test_serialize_list_of_lopsided_enums() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data); let data = [ @@ -139,7 +139,7 @@ fn test_serialize_list_of_lopsided_enums() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data); let data = [ @@ -152,7 +152,7 @@ fn test_serialize_list_of_lopsided_enums() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data); } @@ -171,14 +171,14 @@ fn test_serialize_u8_enum() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); let data = Enum::B; let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); } @@ -198,7 +198,7 @@ fn test_serialize_corrupted_enum() { buf = serialize_const(&data, buf); buf = buf.set(0, 2); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf), None); } @@ -226,7 +226,7 @@ fn test_serialize_nested_enum() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); let data = Enum::B { @@ -236,7 +236,7 @@ fn test_serialize_nested_enum() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); let data = Enum::B { @@ -249,7 +249,7 @@ fn test_serialize_nested_enum() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); let data = Enum::B { @@ -262,6 +262,81 @@ fn test_serialize_nested_enum() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data); } + +#[test] +fn test_adding_enum_field_non_breaking() { + #[derive(Debug, PartialEq, SerializeConst)] + #[repr(C, u8)] + enum Initial { + A { a: u32, b: u8 }, + } + + #[derive(Debug, PartialEq, SerializeConst)] + #[repr(C, u8)] + enum New { + A { b: u8, a: u32, c: u32 }, + } + + let data = New::A { + a: 0x11111111, + b: 0x22, + c: 0x33333333, + }; + let mut buf = ConstVec::new(); + buf = serialize_const(&data, buf); + let buf = buf.as_ref(); + // The new struct should be able to deserialize into the initial struct + let (_, data2) = deserialize_const!(Initial, buf).unwrap(); + assert_eq!( + Initial::A { + a: 0x11111111, + b: 0x22, + }, + data2 + ); +} + +#[test] +fn test_adding_enum_variant_non_breaking() { + #[derive(Debug, PartialEq, SerializeConst)] + #[repr(C, u8)] + enum Initial { + A { a: u32, b: u8 }, + } + + #[derive(Debug, PartialEq, SerializeConst)] + #[repr(C, u8)] + enum New { + #[allow(unused)] + B { + d: u32, + e: u8, + }, + A { + c: u32, + b: u8, + a: u32, + }, + } + + let data = New::A { + a: 0x11111111, + b: 0x22, + c: 0x33333333, + }; + let mut buf = ConstVec::new(); + buf = serialize_const(&data, buf); + let buf = buf.as_ref(); + // The new struct should be able to deserialize into the initial struct + let (_, data2) = deserialize_const!(Initial, buf).unwrap(); + assert_eq!( + Initial::A { + a: 0x11111111, + b: 0x22, + }, + data2 + ); +} diff --git a/packages/const-serialize/tests/lists.rs b/packages/const-serialize/tests/lists.rs index 84f9fe11b2..4192499150 100644 --- a/packages/const-serialize/tests/lists.rs +++ b/packages/const-serialize/tests/lists.rs @@ -5,7 +5,7 @@ fn test_serialize_const_layout_list() { let mut buf = ConstVec::new(); buf = serialize_const(&[1u8, 2, 3] as &[u8; 3], buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!([u8; 3], buf).unwrap().1, [1, 2, 3]) } @@ -17,7 +17,7 @@ fn test_serialize_const_layout_nested_lists() { buf, ); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!( deserialize_const!([[u8; 3]; 3], buf).unwrap().1, @@ -29,6 +29,6 @@ fn test_serialize_const_layout_nested_lists() { fn test_serialize_list_too_little_data() { let mut buf = ConstVec::new(); buf = buf.push(1); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!([u64; 10], buf), None); } diff --git a/packages/const-serialize/tests/primitive.rs b/packages/const-serialize/tests/primitive.rs index a5e3e803ff..0423dcf219 100644 --- a/packages/const-serialize/tests/primitive.rs +++ b/packages/const-serialize/tests/primitive.rs @@ -4,58 +4,34 @@ use const_serialize::{deserialize_const, serialize_const, ConstVec}; fn test_serialize_const_layout_primitive() { let mut buf = ConstVec::new(); buf = serialize_const(&1234u32, buf); - if cfg!(feature = "test-big-endian") { - assert_eq!(buf.as_ref(), 1234u32.to_be_bytes()); - } else { - assert_eq!(buf.as_ref(), 1234u32.to_le_bytes()); - } - let buf = buf.read(); + let buf = buf.as_ref(); + println!("{:?}", buf); assert_eq!(deserialize_const!(u32, buf).unwrap().1, 1234u32); let mut buf = ConstVec::new(); buf = serialize_const(&1234u64, buf); - if cfg!(feature = "test-big-endian") { - assert_eq!(buf.as_ref(), 1234u64.to_be_bytes()); - } else { - assert_eq!(buf.as_ref(), 1234u64.to_le_bytes()); - } - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(u64, buf).unwrap().1, 1234u64); let mut buf = ConstVec::new(); buf = serialize_const(&1234i32, buf); - if cfg!(feature = "test-big-endian") { - assert_eq!(buf.as_ref(), 1234i32.to_be_bytes()); - } else { - assert_eq!(buf.as_ref(), 1234i32.to_le_bytes()); - } - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(i32, buf).unwrap().1, 1234i32); let mut buf = ConstVec::new(); buf = serialize_const(&1234i64, buf); - if cfg!(feature = "test-big-endian") { - assert_eq!(buf.as_ref(), 1234i64.to_be_bytes()); - } else { - assert_eq!(buf.as_ref(), 1234i64.to_le_bytes()); - } - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(i64, buf).unwrap().1, 1234i64); let mut buf = ConstVec::new(); buf = serialize_const(&true, buf); assert_eq!(buf.as_ref(), [1u8]); - let buf = buf.read(); + let buf = buf.as_ref(); assert!(deserialize_const!(bool, buf).unwrap().1); let mut buf = ConstVec::new(); buf = serialize_const(&0.631f32, buf); - if cfg!(feature = "test-big-endian") { - assert_eq!(buf.as_ref(), 0.631f32.to_be_bytes()); - } else { - assert_eq!(buf.as_ref(), 0.631f32.to_le_bytes()); - } - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(f32, buf).unwrap().1, 0.631); } @@ -66,6 +42,6 @@ fn test_serialize_primitive_too_little_data() { buf = buf.push(1); buf = buf.push(1); buf = buf.push(1); - let buf = buf.read(); - assert_eq!(deserialize_const!(u64, buf), None); + let buf = buf.as_ref(); + assert_eq!(deserialize_const!([u64; 10], buf), None); } diff --git a/packages/const-serialize/tests/str.rs b/packages/const-serialize/tests/str.rs index 45371741d5..4a11deeb41 100644 --- a/packages/const-serialize/tests/str.rs +++ b/packages/const-serialize/tests/str.rs @@ -6,11 +6,11 @@ fn test_serialize_const_layout_str() { let str = ConstStr::new("hello"); buf = serialize_const(&str, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); - assert_eq!( - deserialize_const!(ConstStr, buf).unwrap().1.as_str(), - "hello" - ); + let buf = buf.as_ref(); + assert!(buf.len() < 10); + let str = deserialize_const!(ConstStr, buf).unwrap().1; + eprintln!("{str:?}"); + assert_eq!(str.as_str(), "hello"); } #[test] @@ -19,7 +19,8 @@ fn test_serialize_const_layout_nested_str() { let str = ConstStr::new("hello"); buf = serialize_const(&[str, str, str] as &[ConstStr; 3], buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + assert!(buf.len() < 30); + let buf = buf.as_ref(); assert_eq!( deserialize_const!([ConstStr; 3], buf).unwrap().1, @@ -35,6 +36,6 @@ fn test_serialize_const_layout_nested_str() { fn test_serialize_str_too_little_data() { let mut buf = ConstVec::new(); buf = buf.push(1); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!(deserialize_const!(ConstStr, buf), None); } diff --git a/packages/const-serialize/tests/structs.rs b/packages/const-serialize/tests/structs.rs index 68ce249381..cb1f9847d2 100644 --- a/packages/const-serialize/tests/structs.rs +++ b/packages/const-serialize/tests/structs.rs @@ -96,7 +96,7 @@ fn test_serialize_const_layout_struct_list() { const _ASSERT: () = { let mut buf = ConstVec::new(); buf = serialize_const(&DATA, buf); - let buf = buf.read(); + let buf = buf.as_ref(); let [first, second, third] = match deserialize_const!([OtherStruct; 3], buf) { Some((_, data)) => data, None => panic!("data mismatch"), @@ -109,7 +109,7 @@ fn test_serialize_const_layout_struct_list() { let mut buf = ConstVec::new(); const DATA_AGAIN: [[OtherStruct; 3]; 3] = [DATA, DATA, DATA]; buf = serialize_const(&DATA_AGAIN, buf); - let buf = buf.read(); + let buf = buf.as_ref(); let [first, second, third] = match deserialize_const!([[OtherStruct; 3]; 3], buf) { Some((_, data)) => data, None => panic!("data mismatch"), @@ -128,7 +128,7 @@ fn test_serialize_const_layout_struct_list() { let mut buf = ConstVec::new(); buf = serialize_const(&DATA, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); let (_, data2) = deserialize_const!([OtherStruct; 3], buf).unwrap(); assert_eq!(DATA, data2); } @@ -158,7 +158,41 @@ fn test_serialize_const_layout_struct() { let mut buf = ConstVec::new(); buf = serialize_const(&data, buf); println!("{:?}", buf.as_ref()); - let buf = buf.read(); + let buf = buf.as_ref(); let (_, data2) = deserialize_const!(OtherStruct, buf).unwrap(); assert_eq!(data, data2); } + +#[test] +fn test_adding_struct_field_non_breaking() { + #[derive(Debug, PartialEq, SerializeConst)] + struct Initial { + a: u32, + b: u8, + } + + #[derive(Debug, PartialEq, SerializeConst)] + struct New { + c: u32, + b: u8, + a: u32, + } + + let data = New { + a: 0x11111111, + b: 0x22, + c: 0x33333333, + }; + let mut buf = ConstVec::new(); + buf = serialize_const(&data, buf); + let buf = buf.as_ref(); + // The new struct should be able to deserialize into the initial struct + let (_, data2) = deserialize_const!(Initial, buf).unwrap(); + assert_eq!( + Initial { + a: data.a, + b: data.b, + }, + data2 + ); +} diff --git a/packages/const-serialize/tests/tuples.rs b/packages/const-serialize/tests/tuples.rs index 43a036c413..d277d826bf 100644 --- a/packages/const-serialize/tests/tuples.rs +++ b/packages/const-serialize/tests/tuples.rs @@ -4,7 +4,7 @@ use const_serialize::{deserialize_const, serialize_const, ConstVec}; fn test_serialize_const_layout_tuple() { let mut buf = ConstVec::new(); buf = serialize_const(&(1234u32, 5678u16), buf); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!( deserialize_const!((u32, u16), buf).unwrap().1, (1234u32, 5678u16) @@ -12,7 +12,7 @@ fn test_serialize_const_layout_tuple() { let mut buf = ConstVec::new(); buf = serialize_const(&(1234f64, 5678u16, 90u8), buf); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!( deserialize_const!((f64, u16, u8), buf).unwrap().1, (1234f64, 5678u16, 90u8) @@ -20,7 +20,7 @@ fn test_serialize_const_layout_tuple() { let mut buf = ConstVec::new(); buf = serialize_const(&(1234u32, 5678u16, 90u8, 1000000f64), buf); - let buf = buf.read(); + let buf = buf.as_ref(); assert_eq!( deserialize_const!((u32, u16, u8, f64), buf).unwrap().1, (1234u32, 5678u16, 90u8, 1000000f64) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 715c379f27..8ec411fe11 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -25,7 +25,6 @@ serde = "1.0.219" serde_json = "1.0.140" thiserror = { workspace = true } tracing = { workspace = true } -wry = { workspace = true, default-features = false, features = ["os-webview", "protocol", "drag-drop"] } futures-channel = { workspace = true } tokio = { workspace = true, features = [ "sync", @@ -59,6 +58,10 @@ signal-hook = "0.3.18" [target.'cfg(target_os = "linux")'.dependencies] wry = { workspace = true, features = ["os-webview", "protocol", "drag-drop", "linux-body"] } +# add wry for other platforms (macOS, Windows, etc.) +[target.'cfg(all(not(target_os = "android"), not(target_os = "linux")))'.dependencies] +wry = { workspace = true, default-features = false, features = ["os-webview", "protocol", "drag-drop"] } + [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] global-hotkey = "0.7.0" rfd = { version = "0.15.3", default-features = false, features = ["xdg-portal", "tokio"] } @@ -73,6 +76,7 @@ objc_id = "0.1.1" # use rustls on android [target.'cfg(target_os = "android")'.dependencies] +wry = { workspace = true, default-features = false, features = ["os-webview", "protocol", "drag-drop"] } tungstenite = { workspace = true, features = ["rustls"] } jni = "0.21.1" ndk = { version = "0.9.0" } diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 8644885374..94e605feec 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" keywords = ["web", "desktop", "mobile", "gui", "wasm"] -rust-version = "1.80.0" +rust-version = "1.83.0" [dependencies] dioxus-core = { workspace = true } diff --git a/packages/dx-macro-helpers/Cargo.toml b/packages/dx-macro-helpers/Cargo.toml new file mode 100644 index 0000000000..ad37db7749 --- /dev/null +++ b/packages/dx-macro-helpers/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "dx-macro-helpers" +version.workspace = true +edition = "2021" +authors = ["DioxusLabs"] +description = "Shared macro helpers for linker-based binary embedding" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus" +homepage = "https://dioxuslabs.com" + +[lib] +proc-macro = false + +[dependencies] +const-serialize = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + diff --git a/packages/dx-macro-helpers/src/lib.rs b/packages/dx-macro-helpers/src/lib.rs new file mode 100644 index 0000000000..58fe7d617e --- /dev/null +++ b/packages/dx-macro-helpers/src/lib.rs @@ -0,0 +1,62 @@ +//! Shared macro helpers for linker-based binary embedding +//! +//! This crate provides generic utilities for serializing data at compile time +//! and generating linker sections for embedding data in binaries. It can be used +//! by any crate that needs to embed serialized data in executables using linker sections. + +pub use const_serialize::{ConstVec, SerializeConst}; + +/// Copy a slice into a constant sized buffer at compile time +/// +/// This is a generic utility that works with any byte slice and can be used +/// in const contexts to create fixed-size arrays from dynamic slices. +pub const fn copy_bytes(bytes: &[u8]) -> [u8; N] { + let mut out = [0; N]; + let mut i = 0; + while i < N { + out[i] = bytes[i]; + i += 1; + } + out +} + +/// Serialize a value to a const buffer, padding to the specified size +/// +/// This is a generic helper that works with any type implementing `SerializeConst`. +/// It serializes the value and then pads the buffer to the specified memory layout size. +pub const fn serialize_to_const( + value: &T, + memory_layout_size: usize, +) -> ConstVec { + let data = ConstVec::new(); + let mut data = const_serialize::serialize_const(value, data); + // Reserve the maximum size of the type + while data.len() < memory_layout_size { + data = data.push(0); + } + data +} + +/// Serialize a value to a const buffer with a fixed maximum size, padding to the specified size +/// +/// This variant uses a `ConstVec` with a fixed maximum size (e.g., `ConstVec`) +/// and then pads to the specified memory layout size. +/// +/// This function serializes directly into the larger buffer to avoid overflow issues +/// when the serialized data exceeds the default 1024-byte buffer size. +pub const fn serialize_to_const_with_max( + value: &impl SerializeConst, + memory_layout_size: usize, +) -> ConstVec { + // Serialize using the default buffer, then copy into the larger buffer + let serialized = const_serialize::serialize_const(value, ConstVec::new()); + let mut data: ConstVec = ConstVec::new_with_max_size(); + data = data.extend(serialized.as_ref()); + // Reserve the maximum size of the type (pad to MEMORY_LAYOUT size) + while data.len() < memory_layout_size { + data = data.push(0); + } + data +} + +pub mod linker; diff --git a/packages/dx-macro-helpers/src/linker.rs b/packages/dx-macro-helpers/src/linker.rs new file mode 100644 index 0000000000..8a112b1b0d --- /dev/null +++ b/packages/dx-macro-helpers/src/linker.rs @@ -0,0 +1,69 @@ +//! Generic linker section generation for binary embedding +//! +//! This module provides utilities for generating linker sections that embed +//! serialized data in binaries with unique export names. + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; + +/// Generate a linker section for embedding serialized data in the binary +/// +/// This function creates a static array containing serialized data and exports it +/// with a unique symbol name that can be found by build tools. The exported symbol +/// follows the pattern `{prefix}{hash}` and can be extracted from the binary after linking. +/// +/// # Parameters +/// +/// - `item`: The item to serialize (must implement `ToTokens`) +/// - `hash`: Unique hash string for the export name +/// - `prefix`: Export prefix (e.g., `"__MY_CRATE__"`) +/// - `serialize_fn`: Path to the serialization function (as a `TokenStream`) +/// - `copy_bytes_fn`: Path to the `copy_bytes` function (as a `TokenStream`) +/// - `buffer_type`: The type of the buffer (e.g., `ConstVec` or `ConstVec`) +/// - `add_used_attribute`: Whether to add the `#[used]` attribute (some crates need it) +/// +/// # Example +/// +/// ```ignore +/// generate_link_section( +/// my_data, +/// "abc123", +/// "__MY_CRATE__", +/// quote! { my_crate::macro_helpers::serialize_data }, +/// quote! { my_crate::macro_helpers::copy_bytes }, +/// quote! { my_crate::macro_helpers::const_serialize::ConstVec }, +/// false, +/// ) +/// ``` +pub fn generate_link_section( + item: impl ToTokens, + hash: &str, + prefix: &str, + serialize_fn: TokenStream2, + copy_bytes_fn: TokenStream2, + buffer_type: TokenStream2, + add_used_attribute: bool, +) -> TokenStream2 { + let position = proc_macro2::Span::call_site(); + let export_name = syn::LitStr::new(&format!("{}{}", prefix, hash), position); + + let used_attr = if add_used_attribute { + quote! { #[used] } + } else { + quote! {} + }; + + quote! { + // First serialize the item into a constant sized buffer + const __BUFFER: #buffer_type = #serialize_fn(&#item); + // Then pull out the byte slice + const __BYTES: &[u8] = __BUFFER.as_ref(); + // And the length of the byte slice + const __LEN: usize = __BYTES.len(); + + // Now that we have the size of the item, copy the bytes into a static array + #used_attr + #[unsafe(export_name = #export_name)] + static __LINK_SECTION: [u8; __LEN] = #copy_bytes_fn(__BYTES); + } +} diff --git a/packages/generational-box/Cargo.toml b/packages/generational-box/Cargo.toml index 686c8c7c6c..3fb9180dbc 100644 --- a/packages/generational-box/Cargo.toml +++ b/packages/generational-box/Cargo.toml @@ -7,7 +7,7 @@ description = "A box backed by a generational runtime" license = "MIT OR Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" keywords = ["generational", "box", "memory", "allocator"] -rust-version = "1.80.0" +rust-version = "1.83.0" [dependencies] parking_lot = { workspace = true } diff --git a/packages/geolocation/Cargo.toml b/packages/geolocation/Cargo.toml new file mode 100644 index 0000000000..663562ce36 --- /dev/null +++ b/packages/geolocation/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "dioxus-geolocation" +description = "Get and track the device's current position for Dioxus mobile apps" +version = { workspace = true } +edition = "2021" +authors = ["DioxusLabs"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus" +build = "build.rs" + +[package.metadata.docs.rs] +targets = ["aarch64-linux-android", "aarch64-apple-ios"] + +[package.metadata.platforms.support] +windows = { level = "none", notes = "" } +linux = { level = "none", notes = "" } +macos = { level = "none", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[features] +default = [] +metadata = ["dioxus-platform-bridge/metadata"] + +[dependencies] +serde = "1.0" +serde_json = "1.0" +log = "0.4" +thiserror = "1.0" +dioxus-platform-bridge = { workspace = true } +permissions = { workspace = true } + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.21" + +[target.'cfg(target_os = "ios")'.dependencies] +objc2 = "0.6.3" + +[build-dependencies] +dioxus-mobile-plugin-build = { workspace = true } diff --git a/packages/geolocation/README.md b/packages/geolocation/README.md new file mode 100644 index 0000000000..ce6fe985b6 --- /dev/null +++ b/packages/geolocation/README.md @@ -0,0 +1,210 @@ +# Dioxus Geolocation Plugin + +Get and track the device's current position, including information about altitude, heading, and speed (if available). + +| Platform | Supported | +| -------- | --------- | +| Linux | βœ— | +| Windows | βœ— | +| macOS | βœ— | +| Android | βœ“ | +| iOS | βœ“ | + +## Installation + +Add the following to your `Cargo.toml` file: + +```toml +[dependencies] +dioxus-geolocation = { path = "../path/to/packages/geolocation" } +# or from crates.io when published: +# dioxus-geolocation = "0.7.0-rc.3" +``` + +## Platform Setup + +### iOS + +Apple requires privacy descriptions to be specified in `Info.plist` for location information: + +- `NSLocationWhenInUseDescription` + +### Permissions + +This plugin uses the Dioxus permissions crate to declare required permissions. The permissions are automatically embedded in the binary and can be extracted by build tools. + +The plugin declares the following permissions: +- **Fine Location**: `ACCESS_FINE_LOCATION` (Android) / `NSLocationWhenInUseUsageDescription` (iOS) +- **Coarse Location**: `ACCESS_COARSE_LOCATION` (Android) / `NSLocationWhenInUseUsageDescription` (iOS) + +#### Android + +If your app requires GPS functionality to function, add the following to your `AndroidManifest.xml`: + +```xml + +``` + +The Google Play Store uses this property to decide whether it should show the app to devices without GPS capabilities. + +**Note**: The location permissions are automatically added by the Dioxus CLI when building your app, as they are declared using the `permissions` crate. + +### Swift Files (iOS/macOS) + +This plugin uses the Dioxus platform bridge to declare Swift source files. The Swift files are automatically embedded in the binary and can be extracted by build tools. + +The plugin declares the following Swift files: +- `ios/Sources/GeolocationPlugin.swift` + +**Note**: Swift files are automatically copied to the iOS/macOS app bundle by the Dioxus CLI when building your app, as they are declared using the `ios_plugin!()` macro. + +## Usage + +### Basic Example + +```rust +use dioxus::prelude::*; +use dioxus_geolocation::{Geolocation, PositionOptions, PermissionState}; + +fn App() -> Element { + let mut geolocation = use_signal(|| Geolocation::new()); + + rsx! { + button { + onclick: move |_| async move { + // Check permissions + let status = geolocation.write().check_permissions().unwrap(); + + if status.location == PermissionState::Prompt { + // Request permissions + let _ = geolocation.write().request_permissions(None).unwrap(); + } + + // Get current position + let options = PositionOptions { + enable_high_accuracy: true, + timeout: 10000, + maximum_age: 0, + }; + + match geolocation.write().get_current_position(Some(options)) { + Ok(position) => { + println!("Latitude: {}, Longitude: {}", + position.coords.latitude, + position.coords.longitude + ); + } + Err(e) => { + eprintln!("Error getting position: {}", e); + } + } + }, + "Get Current Position" + } + } +} +``` + +### Checking and Requesting Permissions + +```rust +use dioxus::prelude::*; +use dioxus_geolocation::{Geolocation, PermissionState}; + +fn App() -> Element { + let mut geolocation = use_signal(|| Geolocation::new()); + let permission_status = use_signal(|| None::); + + rsx! { + button { + onclick: move |_| async move { + match geolocation.write().check_permissions() { + Ok(status) => { + let msg = format!( + "Location: {:?}, Coarse: {:?}", + status.location, status.coarse_location + ); + permission_status.set(Some(msg)); + + if status.location == PermissionState::Prompt { + // Request permissions + if let Ok(new_status) = geolocation.write().request_permissions(None) { + let msg = format!( + "After request - Location: {:?}, Coarse: {:?}", + new_status.location, new_status.coarse_location + ); + permission_status.set(Some(msg)); + } + } + } + Err(e) => { + permission_status.set(Some(format!("Error: {}", e))); + } + } + }, + "Check Permissions" + } + + if let Some(status) = permission_status.read().as_ref() { + p { "{status}" } + } + } +} +``` + +## API Reference + +### `Geolocation` + +Main entry point for geolocation functionality. + +#### Methods + +- `new() -> Geolocation` - Create a new Geolocation instance +- `get_current_position(options: Option) -> Result` - Get current position +- `check_permissions() -> Result` - Check current permission status +- `request_permissions(permissions: Option>) -> Result` - Request permissions + +### Types + +- `PositionOptions` - Options for retrieving the current position + - `enable_high_accuracy: bool` - Use high accuracy mode (GPS) + - `timeout: u32` - Maximum wait time in milliseconds + - `maximum_age: u32` - Maximum age of cached position in milliseconds + +- `Position` - Current position data + - `timestamp: u64` - Timestamp in milliseconds + - `coords: Coordinates` - Coordinate data + +- `Coordinates` - Coordinate information + - `latitude: f64` - Latitude in decimal degrees + - `longitude: f64` - Longitude in decimal degrees + - `accuracy: f64` - Accuracy in meters + - `altitude: Option` - Altitude in meters (if available) + - `altitude_accuracy: Option` - Altitude accuracy in meters (if available) + - `speed: Option` - Speed in m/s (if available) + - `heading: Option` - Heading in degrees (if available) + +- `PermissionStatus` - Permission status + - `location: PermissionState` - Location permission state + - `coarse_location: PermissionState` - Coarse location permission state + +- `PermissionState` - Permission state enum + - `Granted` - Permission granted + - `Denied` - Permission denied + - `Prompt` - Permission not yet determined + - `PromptWithRationale` - Permission prompt with rationale (Android 12+) + + +## Architecture + +This plugin uses Dioxus's platform bridge for Android/iOS integration: + +- **Android**: Uses JNI bindings via `dioxus-platform-bridge` to call Kotlin code +- **iOS**: Uses ObjC bindings via `dioxus-platform-bridge` to call Swift code + +The native Kotlin/Swift code is designed to be reusable with Tauri plugins, allowing code sharing between Dioxus and Tauri implementations. + +## License + +MIT OR Apache-2.0 diff --git a/packages/geolocation/android/build.gradle.kts b/packages/geolocation/android/build.gradle.kts new file mode 100644 index 0000000000..b8aaa7372f --- /dev/null +++ b/packages/geolocation/android/build.gradle.kts @@ -0,0 +1,44 @@ +import org.gradle.api.tasks.bundling.AbstractArchiveTask + +plugins { + id("com.android.library") version "8.4.2" + kotlin("android") version "1.9.24" +} + +android { + namespace = "app.tauri.geolocation" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + targetSdk = 34 + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + getByName("debug") { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("com.google.android.gms:play-services-location:21.3.0") +} + +tasks.withType().configureEach { + archiveBaseName.set("geolocation-plugin") +} diff --git a/packages/geolocation/android/consumer-rules.pro b/packages/geolocation/android/consumer-rules.pro new file mode 100644 index 0000000000..7b3a455527 --- /dev/null +++ b/packages/geolocation/android/consumer-rules.pro @@ -0,0 +1 @@ +# Intentionally empty; no consumer Proguard rules required for the geolocation plugin. diff --git a/packages/geolocation/android/gradle.properties b/packages/geolocation/android/gradle.properties new file mode 100644 index 0000000000..cccbfe6f22 --- /dev/null +++ b/packages/geolocation/android/gradle.properties @@ -0,0 +1,25 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false diff --git a/packages/geolocation/android/gradle/wrapper/gradle-wrapper.jar b/packages/geolocation/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000000..8bdaf60c75 Binary files /dev/null and b/packages/geolocation/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/geolocation/android/gradle/wrapper/gradle-wrapper.properties b/packages/geolocation/android/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000000..4ee6141a80 --- /dev/null +++ b/packages/geolocation/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +## TODO: Update this before merging +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +# distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/geolocation/android/gradlew b/packages/geolocation/android/gradlew new file mode 100755 index 0000000000..adff685a03 --- /dev/null +++ b/packages/geolocation/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright Β© 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions Β«$varΒ», Β«${var}Β», Β«${var:-default}Β», Β«${var+SET}Β», +# Β«${var#prefix}Β», Β«${var%suffix}Β», and Β«$( cmd )Β»; +# * compound commands having a testable exit status, especially Β«caseΒ»; +# * various built-in commands including Β«commandΒ», Β«setΒ», and Β«ulimitΒ». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/packages/geolocation/android/gradlew.bat b/packages/geolocation/android/gradlew.bat new file mode 100644 index 0000000000..e509b2dd8f --- /dev/null +++ b/packages/geolocation/android/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/geolocation/android/settings.gradle.kts b/packages/geolocation/android/settings.gradle.kts new file mode 100644 index 0000000000..be19f48150 --- /dev/null +++ b/packages/geolocation/android/settings.gradle.kts @@ -0,0 +1,19 @@ +import org.gradle.api.initialization.resolve.RepositoriesMode + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "geolocation-android" diff --git a/packages/geolocation/android/src/main/AndroidManifest.xml b/packages/geolocation/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1cbe549e60 --- /dev/null +++ b/packages/geolocation/android/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/geolocation/android/src/main/java/app/tauri/geolocation/Geolocation.kt b/packages/geolocation/android/src/main/java/app/tauri/geolocation/Geolocation.kt new file mode 100644 index 0000000000..be99c58494 --- /dev/null +++ b/packages/geolocation/android/src/main/java/app/tauri/geolocation/Geolocation.kt @@ -0,0 +1,87 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.geolocation + +import android.annotation.SuppressLint +import android.content.Context +import android.location.Location +import android.location.LocationManager +import android.os.SystemClock +import androidx.core.location.LocationManagerCompat +import android.util.Log +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority + +class Geolocation(private val context: Context) { + fun isLocationServicesEnabled(): Boolean { + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return LocationManagerCompat.isLocationEnabled(lm) + } + + @SuppressWarnings("MissingPermission") + fun sendLocation( + enableHighAccuracy: Boolean, + successCallback: (location: Location) -> Unit, + errorCallback: (error: String) -> Unit, + ) { + val resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) + if (resultCode == ConnectionResult.SUCCESS) { + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + if (this.isLocationServicesEnabled()) { + var networkEnabled = false + + try { + networkEnabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } catch (_: Exception) { + Log.e("Geolocation", "isProviderEnabled failed") + } + + val lowPrio = + if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER + val prio = if (enableHighAccuracy) Priority.PRIORITY_HIGH_ACCURACY else lowPrio + + Log.d("Geolocation", "Using priority $prio") + + LocationServices + .getFusedLocationProviderClient(context) + .getCurrentLocation(prio, null) + .addOnFailureListener { e -> e.message?.let { errorCallback(it) } } + .addOnSuccessListener { location -> + if (location == null) { + errorCallback("Location unavailable.") + } else { + successCallback(location) + } + } + } else { + errorCallback("Location disabled.") + } + } else { + errorCallback("Google Play Services unavailable.") + } + } + + @SuppressLint("MissingPermission") + fun getLastLocation(maximumAge: Long): Location? { + var lastLoc: Location? = null + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + for (provider in lm.allProviders) { + val tmpLoc = lm.getLastKnownLocation(provider) + if (tmpLoc != null) { + val locationAge = SystemClock.elapsedRealtimeNanos() - tmpLoc.elapsedRealtimeNanos + val maxAgeNano = maximumAge * 1_000_000L + if (locationAge <= maxAgeNano && (lastLoc == null || lastLoc.elapsedRealtimeNanos > tmpLoc.elapsedRealtimeNanos)) { + lastLoc = tmpLoc + } + } + } + + return lastLoc + } +} diff --git a/packages/geolocation/android/src/main/java/app/tauri/geolocation/GeolocationPlugin.kt b/packages/geolocation/android/src/main/java/app/tauri/geolocation/GeolocationPlugin.kt new file mode 100644 index 0000000000..4b1f20b166 --- /dev/null +++ b/packages/geolocation/android/src/main/java/app/tauri/geolocation/GeolocationPlugin.kt @@ -0,0 +1,173 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.geolocation + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.location.Location +import android.os.Handler +import android.os.Looper +import android.webkit.WebView +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import org.json.JSONObject +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.Timer +import kotlin.concurrent.schedule + +class GeolocationPlugin(private val activity: Activity) { + private val geolocation = Geolocation(activity) + + fun checkPermissions(): Map { + val response = mutableMapOf() + val coarseStatus = ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) + val fineStatus = ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) + + response["location"] = permissionToStatus(fineStatus) + response["coarseLocation"] = permissionToStatus(coarseStatus) + + return response + } + + fun requestPermissions(callback: (Map) -> Unit) { + val permissionsToRequest = mutableListOf() + + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION) + } + + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + permissionsToRequest.add(Manifest.permission.ACCESS_COARSE_LOCATION) + } + + if (permissionsToRequest.isEmpty()) { + callback(checkPermissions()) + } else { + ActivityCompat.requestPermissions(activity, permissionsToRequest.toTypedArray(), 1001) + Handler(Looper.getMainLooper()).postDelayed({ callback(checkPermissions()) }, 1000) + } + } + + fun getCurrentPosition( + enableHighAccuracy: Boolean, + timeout: Long, + maximumAge: Long, + successCallback: (Location) -> Unit, + errorCallback: (String) -> Unit, + ) { + val lastLocation = geolocation.getLastLocation(maximumAge) + if (lastLocation != null) { + successCallback(lastLocation) + return + } + + val timer = Timer() + timer.schedule(timeout) { + activity.runOnUiThread { errorCallback("Timeout waiting for location.") } + } + + geolocation.sendLocation( + enableHighAccuracy, + { location -> + timer.cancel() + successCallback(location) + }, + { error -> + timer.cancel() + errorCallback(error) + }, + ) + } + + private fun permissionToStatus(value: Int): String = + when (value) { + PackageManager.PERMISSION_GRANTED -> "granted" + PackageManager.PERMISSION_DENIED -> "denied" + else -> "prompt" + } + + // ---- Platform bridge helpers expected by Rust JNI layer ---- + + // Called by Rust after constructing the plugin. No-op placeholder to match signature. + fun load(webView: WebView?) { /* no-op */ } + + // Serialize current permission status as JSON string + fun checkPermissionsJson(): String { + val status = checkPermissions() + val json = JSONObject() + json.put("location", status["location"]) // granted|denied|prompt + json.put("coarseLocation", status["coarseLocation"]) // granted|denied|prompt + return json.toString() + } + + // Request permissions and return resulting status JSON (waits briefly for result) + fun requestPermissionsJson(permissionsJson: String?): String { + val latch = CountDownLatch(1) + var result: String = checkPermissionsJson() + + requestPermissions { status -> + val json = JSONObject() + json.put("location", status["location"]) + json.put("coarseLocation", status["coarseLocation"]) + result = json.toString() + latch.countDown() + } + + // Wait up to 5 seconds for the permission result, then return whatever we have + latch.await(5, TimeUnit.SECONDS) + return result + } + + // Convert a Location to the Position JSON expected by Rust side + private fun locationToPositionJson(location: Location): String { + val coords = JSONObject() + coords.put("latitude", location.latitude) + coords.put("longitude", location.longitude) + coords.put("accuracy", location.accuracy.toDouble()) + if (location.hasAltitude()) coords.put("altitude", location.altitude) + if (android.os.Build.VERSION.SDK_INT >= 26) { + val vAcc = try { location.verticalAccuracyMeters } catch (_: Exception) { null } + if (vAcc != null) coords.put("altitudeAccuracy", vAcc.toDouble()) + } + if (location.hasSpeed()) coords.put("speed", location.speed.toDouble()) + if (location.hasBearing()) coords.put("heading", location.bearing.toDouble()) + + val obj = JSONObject() + obj.put("timestamp", System.currentTimeMillis()) + obj.put("coords", coords) + return obj.toString() + } + + // Synchronous wrapper returning JSON for getCurrentPosition + fun getCurrentPositionJson(options: Map): String { + val enableHighAccuracy = (options["enableHighAccuracy"] as? Boolean) ?: false + val timeout = (options["timeout"] as? Number)?.toLong() ?: 10000L + val maximumAge = (options["maximumAge"] as? Number)?.toLong() ?: 0L + + var output: String? = null + val latch = CountDownLatch(1) + + getCurrentPosition( + enableHighAccuracy, + timeout, + maximumAge, + { location -> + output = locationToPositionJson(location) + latch.countDown() + }, + { error -> + output = JSONObject(mapOf("error" to error)).toString() + latch.countDown() + }, + ) + + // Wait up to the timeout + 2s buffer + latch.await(timeout + 2000, TimeUnit.MILLISECONDS) + return output ?: JSONObject(mapOf("error" to "Timeout waiting for location.")).toString() + } + +} diff --git a/packages/geolocation/build.rs b/packages/geolocation/build.rs new file mode 100644 index 0000000000..c290b75dc4 --- /dev/null +++ b/packages/geolocation/build.rs @@ -0,0 +1,45 @@ +use std::{env, path::PathBuf}; + +use dioxus_mobile_plugin_build::{ + build_android_library, build_swift_package, AndroidLibraryConfig, SwiftPackageConfig, +}; + +const SWIFT_PRODUCT: &str = "GeolocationPlugin"; +const SWIFT_MIN_IOS: &str = "13.0"; +const ANDROID_AAR_PREFERRED: &str = "android/build/outputs/aar/geolocation-plugin-release.aar"; + +fn main() { + println!("cargo:rerun-if-changed=ios/Package.swift"); + println!("cargo:rerun-if-changed=ios/Sources/GeolocationPlugin.swift"); + println!("cargo:rerun-if-changed=android/build.gradle.kts"); + println!("cargo:rerun-if-changed=android/settings.gradle.kts"); + println!("cargo:rerun-if-changed=android/src"); + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let swift_package_dir = manifest_dir.join("ios"); + let android_project_dir = manifest_dir.join("android"); + let preferred_aar = manifest_dir.join(ANDROID_AAR_PREFERRED); + + if let Err(err) = build_swift_package(&SwiftPackageConfig { + product: SWIFT_PRODUCT, + min_ios_version: SWIFT_MIN_IOS, + package_dir: &swift_package_dir, + link_frameworks: &["CoreLocation", "Foundation"], + link_libraries: &[ + "swiftCompatibility56", + "swiftCompatibilityConcurrency", + "swiftCompatibilityPacks", + ], + }) { + panic!("Failed to build Swift plugin: {err}"); + } + + if let Err(err) = build_android_library(&AndroidLibraryConfig { + project_dir: &android_project_dir, + preferred_artifact: &preferred_aar, + artifact_env_key: "DIOXUS_ANDROID_ARTIFACT", + gradle_task: "assembleRelease", + }) { + panic!("Failed to build Android plugin: {err}"); + } +} diff --git a/packages/geolocation/ios/.gitignore b/packages/geolocation/ios/.gitignore new file mode 100644 index 0000000000..5922fdaa56 --- /dev/null +++ b/packages/geolocation/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/packages/geolocation/ios/Package.swift b/packages/geolocation/ios/Package.swift new file mode 100644 index 0000000000..ebbb0d0f9a --- /dev/null +++ b/packages/geolocation/ios/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.7 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "GeolocationPlugin", + platforms: [ + .iOS(.v13), + .macOS(.v12), + ], + products: [ + .library( + name: "GeolocationPlugin", + type: .static, + targets: ["GeolocationPlugin"]) + ], + dependencies: [], + targets: [ + .target( + name: "GeolocationPlugin", + path: "Sources", + linkerSettings: [ + .linkedFramework("CoreLocation"), + .linkedFramework("Foundation"), + ]) + ] +) diff --git a/packages/geolocation/ios/README.md b/packages/geolocation/ios/README.md new file mode 100644 index 0000000000..5612ac827c --- /dev/null +++ b/packages/geolocation/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin Geolocation + +A description of this package. diff --git a/packages/geolocation/ios/Sources/GeolocationPlugin.swift b/packages/geolocation/ios/Sources/GeolocationPlugin.swift new file mode 100644 index 0000000000..39299a8838 --- /dev/null +++ b/packages/geolocation/ios/Sources/GeolocationPlugin.swift @@ -0,0 +1,220 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import CoreLocation +import Foundation +import Dispatch + +/** + * Simplified GeolocationPlugin for Dioxus that works without Tauri dependencies. + * This can be shared with Tauri plugins with minimal changes. + */ +@objc(GeolocationPlugin) +public class GeolocationPlugin: NSObject, CLLocationManagerDelegate { + private let locationManager = CLLocationManager() + private var positionCallbacks: [String: (String) -> Void] = [:] + + override init() { + super.init() + locationManager.delegate = self + } + + /** + * Get current position as JSON string (called from ObjC/Rust) + */ + @objc public func getCurrentPositionJson(_ optionsJson: String) -> String { + // Parse options from JSON + guard let optionsData = optionsJson.data(using: .utf8), + let optionsDict = try? JSONSerialization.jsonObject(with: optionsData) as? [String: Any] else { + let error = ["error": "Invalid options JSON"] + return (try? JSONSerialization.data(withJSONObject: error))?.base64EncodedString() ?? "" + } + + let enableHighAccuracy = optionsDict["enableHighAccuracy"] as? Bool ?? false + let timeoutMs = optionsDict["timeout"] as? Double ?? 10000 + let maximumAgeMs = optionsDict["maximumAge"] as? Double ?? 0 + + // If we have a recent cached location, return it immediately + if let lastLocation = self.locationManager.location { + let ageMs = Date().timeIntervalSince(lastLocation.timestamp) * 1000 + if maximumAgeMs <= 0 || ageMs <= maximumAgeMs { + return self.convertLocationToJson(lastLocation) + } + } + + let callbackId = UUID().uuidString + let semaphore = DispatchSemaphore(value: 0) + var responseJson: String? + + self.positionCallbacks[callbackId] = { result in + responseJson = result + semaphore.signal() + } + + if enableHighAccuracy { + self.locationManager.desiredAccuracy = kCLLocationAccuracyBest + } else { + self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer + } + + if CLLocationManager.authorizationStatus() == .notDetermined { + self.locationManager.requestWhenInUseAuthorization() + } else { + self.locationManager.requestLocation() + } + + let timeoutSeconds = max(timeoutMs / 1000.0, 0.1) + let deadline = Date().addingTimeInterval(timeoutSeconds) + while responseJson == nil && Date() < deadline { + let _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05)) + if semaphore.wait(timeout: .now()) == .success { + break + } + } + + if let json = responseJson { + return json + } else { + // Timed out waiting for location + self.positionCallbacks.removeValue(forKey: callbackId) + let error = ["error": "Timeout waiting for location"] + return (try? JSONSerialization.data(withJSONObject: error)).flatMap { + String(data: $0, encoding: .utf8) + } ?? "{\"error\":\"Timeout waiting for location\"}" + } + } + + /** + * Check permissions and return JSON string (called from ObjC/Rust) + */ + @objc public func checkPermissionsJson() -> String { + var status: String = "" + + if CLLocationManager.locationServicesEnabled() { + switch CLLocationManager.authorizationStatus() { + case .notDetermined: + status = "prompt" + case .restricted, .denied: + status = "denied" + case .authorizedAlways, .authorizedWhenInUse: + status = "granted" + @unknown default: + status = "prompt" + } + } else { + let error = ["error": "Location services are not enabled"] + return (try? JSONSerialization.data(withJSONObject: error))?.base64EncodedString() ?? "" + } + + let result: [String: String] = ["location": status, "coarseLocation": status] + + if let jsonData = try? JSONSerialization.data(withJSONObject: result), + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } + + return "" + } + + /** + * Request permissions and return JSON string (called from ObjC/Rust) + */ + @objc public func requestPermissionsJson(_ permissionsJson: String) -> String { + if CLLocationManager.locationServicesEnabled() { + if CLLocationManager.authorizationStatus() == .notDetermined { + DispatchQueue.main.async { + self.locationManager.requestWhenInUseAuthorization() + } + // Return current status - actual result comes via delegate + return self.checkPermissionsJson() + } else { + return self.checkPermissionsJson() + } + } else { + let error = ["error": "Location services are not enabled"] + if let jsonData = try? JSONSerialization.data(withJSONObject: error), + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } + return "" + } + } + + // + // CLLocationManagerDelegate methods + // + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + let errorMessage = error.localizedDescription + + // Notify all position callbacks + for (_, callback) in self.positionCallbacks { + let errorJson = "{\"error\":\"\(errorMessage)\"}" + callback(errorJson) + } + self.positionCallbacks.removeAll() + + } + + public func locationManager( + _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] + ) { + guard let location = locations.last else { + return + } + + let resultJson = self.convertLocationToJson(location) + + // Notify all position callbacks + for (_, callback) in self.positionCallbacks { + callback(resultJson) + } + self.positionCallbacks.removeAll() + + } + + public func locationManager( + _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus + ) { + if !self.positionCallbacks.isEmpty { + self.locationManager.requestLocation() + } + } + + // + // Internal/Helper methods + // + + private func convertLocationToJson(_ location: CLLocation) -> String { + var ret: [String: Any] = [:] + var coords: [String: Any] = [:] + + coords["latitude"] = location.coordinate.latitude + coords["longitude"] = location.coordinate.longitude + coords["accuracy"] = location.horizontalAccuracy + coords["altitude"] = location.altitude + coords["altitudeAccuracy"] = location.verticalAccuracy + coords["speed"] = location.speed + coords["heading"] = location.course + ret["timestamp"] = Int((location.timestamp.timeIntervalSince1970 * 1000)) + ret["coords"] = coords + + if let jsonData = try? JSONSerialization.data(withJSONObject: ret), + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } + + return "{\"error\":\"Failed to serialize location\"}" + } + +} + +/** + * Anchor function to force the Swift object file into linked binaries. + * Rust calls this symbol at startup to ensure the class is registered with the ObjC runtime. + */ +@_cdecl("dioxus_geolocation_plugin_init") +public func dioxus_geolocation_plugin_init() { + _ = GeolocationPlugin.self +} diff --git a/packages/geolocation/ios/Tests/PluginTests/PluginTests.swift b/packages/geolocation/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000000..99992ce4c3 --- /dev/null +++ b/packages/geolocation/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/packages/geolocation/src/android.rs b/packages/geolocation/src/android.rs new file mode 100644 index 0000000000..67dbd82e11 --- /dev/null +++ b/packages/geolocation/src/android.rs @@ -0,0 +1,230 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use dioxus_platform_bridge::android::with_activity; +use jni::{ + objects::{GlobalRef, JObject, JString, JValue}, + JNIEnv, +}; +use serde_json::Value as JsonValue; + +use crate::error::{Error, Result}; +use crate::models::*; + +const PLUGIN_CLASS: &str = "app/tauri/geolocation/GeolocationPlugin"; + +/// Android implementation of the geolocation API +pub struct Geolocation { + plugin_instance: Option, +} + +impl Geolocation { + /// Create a new Geolocation instance + pub fn new() -> Self { + Self { + plugin_instance: None, + } + } + + /// Initialize the plugin and get an instance + fn get_plugin_instance(&mut self, env: &mut JNIEnv) -> Result { + if let Some(ref instance) = self.plugin_instance { + Ok(instance.clone()) + } else { + let _plugin_class = env.find_class(PLUGIN_CLASS)?; + + // Create a new instance - we need to get the activity first + let instance = with_activity(|env, activity| { + // Call constructor: GeolocationPlugin(Activity) + let plugin_obj = env + .new_object( + PLUGIN_CLASS, + "(Landroid/app/Activity;)V", + &[JValue::Object(activity)], + ) + .ok()?; + + // Call load method with null WebView for now (not needed for Dioxus) + let null_webview = JObject::null(); + env.call_method( + &plugin_obj, + "load", + "(Landroid/webkit/WebView;)V", + &[JValue::Object(&null_webview)], + ) + .ok()?; + + Some(env.new_global_ref(&plugin_obj).ok()?) + }) + .ok_or_else(|| Error::PlatformBridge("Failed to create plugin instance".to_string()))?; + + self.plugin_instance = Some(instance.clone()); + Ok(instance) + } + } + + /// Get current position + pub fn get_current_position(&mut self, options: Option) -> Result { + let options = options.unwrap_or_default(); + + with_activity(|env, _activity| { + let plugin = self.get_plugin_instance(env).ok()?; + + // Create a Java HashMap with the options + let options_map = env.new_object("java/util/HashMap", "()V", &[]).ok()?; + + // Put enableHighAccuracy + let key_acc = env.new_string("enableHighAccuracy").ok()?; + // Create java.lang.Boolean from Rust bool + let val_acc_obj = env + .call_static_method( + "java/lang/Boolean", + "valueOf", + "(Z)Ljava/lang/Boolean;", + &[JValue::Bool(if options.enable_high_accuracy { + 1 + } else { + 0 + })], + ) + .ok()? + .l() + .ok()?; + env.call_method( + &options_map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[JValue::Object(&key_acc), JValue::Object(&val_acc_obj)], + ) + .ok()?; + + // Put maximumAge + let key_age = env.new_string("maximumAge").ok()?; + let val_age_obj = env + .call_static_method( + "java/lang/Long", + "valueOf", + "(J)Ljava/lang/Long;", + &[JValue::Long(options.maximum_age as i64)], + ) + .ok()? + .l() + .ok()?; + env.call_method( + &options_map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[JValue::Object(&key_age), JValue::Object(&val_age_obj)], + ) + .ok()?; + + // Put timeout + let key_timeout = env.new_string("timeout").ok()?; + let val_timeout_obj = env + .call_static_method( + "java/lang/Long", + "valueOf", + "(J)Ljava/lang/Long;", + &[JValue::Long(options.timeout as i64)], + ) + .ok()? + .l() + .ok()?; + env.call_method( + &options_map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[ + JValue::Object(&key_timeout), + JValue::Object(&val_timeout_obj), + ], + ) + .ok()?; + + // Call getCurrentPositionJson(Map): String + let result = env + .call_method( + &plugin, + "getCurrentPositionJson", + "(Ljava/util/Map;)Ljava/lang/String;", + &[JValue::Object(&options_map)], + ) + .ok()?; + + let jstr_obj = result.l().ok()?; + let jstr: JString = JString::from(jstr_obj); + let result_string: String = env.get_string(&jstr).ok()?.into(); + + // Deserialize the JSON result + let json_value: JsonValue = serde_json::from_str(&result_string).ok()?; + + // Check if it's an error + if let Some(error_msg) = json_value.get("error") { + return Some(Err(Error::LocationUnavailable( + error_msg.as_str().unwrap_or("Unknown error").to_string(), + ))); + } + + let position: Position = serde_json::from_value(json_value).ok()?; + Some(Ok(position)) + }) + .ok_or_else(|| Error::PlatformBridge("Failed to get current position".to_string()))? + } + + /// Check permissions + pub fn check_permissions(&mut self) -> Result { + with_activity(|env, _activity| { + let plugin = self.get_plugin_instance(env).ok()?; + + let result = env + .call_method(&plugin, "checkPermissionsJson", "()Ljava/lang/String;", &[]) + .ok()?; + + let jstr_obj = result.l().ok()?; + let jstr: JString = JString::from(jstr_obj); + let result_string: String = env.get_string(&jstr).ok()?.into(); + + let status: PermissionStatus = serde_json::from_str(&result_string).ok()?; + Some(Ok(status)) + }) + .ok_or_else(|| Error::PlatformBridge("Failed to check permissions".to_string()))? + } + + /// Request permissions + pub fn request_permissions( + &mut self, + permissions: Option>, + ) -> Result { + with_activity(|env, _activity| { + let plugin = self.get_plugin_instance(env).ok()?; + + // Serialize permissions to JSON + let perms_json = serde_json::to_string(&permissions).ok()?; + let perms_string = env.new_string(&perms_json).ok()?; + + let result = env + .call_method( + &plugin, + "requestPermissionsJson", + "(Ljava/lang/String;)Ljava/lang/String;", + &[JValue::Object(&perms_string)], + ) + .ok()?; + + let jstr_obj = result.l().ok()?; + let jstr: JString = JString::from(jstr_obj); + let result_string: String = env.get_string(&jstr).ok()?.into(); + + let status: PermissionStatus = serde_json::from_str(&result_string).ok()?; + Some(Ok(status)) + }) + .ok_or_else(|| Error::PlatformBridge("Failed to request permissions".to_string()))? + } +} + +impl Default for Geolocation { + fn default() -> Self { + Self::new() + } +} diff --git a/packages/geolocation/src/error.rs b/packages/geolocation/src/error.rs new file mode 100644 index 0000000000..bd0d19d872 --- /dev/null +++ b/packages/geolocation/src/error.rs @@ -0,0 +1,53 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Android-specific error + #[cfg(target_os = "android")] + #[error("Android error: {0}")] + Android(#[from] jni::errors::Error), + + /// iOS-specific error + #[cfg(target_os = "ios")] + #[error("iOS error: {0}")] + Ios(String), + + /// JSON serialization/deserialization error + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + /// Platform bridge error + #[error("Platform bridge error: {0}")] + PlatformBridge(String), + + /// Location services are disabled + #[error("Location services are disabled")] + LocationServicesDisabled, + + /// Permission denied + #[error("Permission denied")] + PermissionDenied, + + /// Location unavailable + #[error("Location unavailable: {0}")] + LocationUnavailable(String), + + /// Timeout waiting for location + #[error("Timeout waiting for location")] + Timeout, +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/packages/geolocation/src/ios.rs b/packages/geolocation/src/ios.rs new file mode 100644 index 0000000000..f6cc9f448c --- /dev/null +++ b/packages/geolocation/src/ios.rs @@ -0,0 +1,170 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use dioxus_platform_bridge::darwin::MainThreadCell; +use objc2::{msg_send, MainThreadMarker}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +use crate::error::{Error, Result}; +use crate::models::*; + +extern "C" { + fn dioxus_geolocation_plugin_init(); +} + +/// iOS implementation of the geolocation API +pub struct Geolocation { + plugin_instance: MainThreadCell<*mut objc2::runtime::Object>, +} + +unsafe impl Send for Geolocation {} +unsafe impl Sync for Geolocation {} + +impl Geolocation { + /// Create a new Geolocation instance + pub fn new() -> Self { + unsafe { + // Ensure the Swift static library is linked and the class is registered + dioxus_geolocation_plugin_init(); + } + + Self { + plugin_instance: MainThreadCell::new(), + } + } + + /// Get or initialize the plugin instance + fn get_plugin_instance(&self, mtm: MainThreadMarker) -> Result<&mut objc2::runtime::Object> { + unsafe { + let ptr_ref = self.plugin_instance.get_or_try_init_with(mtm, || { + let class_name = + CStr::from_bytes_with_nul(b"GeolocationPlugin\0").expect("Invalid class name"); + let class = objc2::runtime::Class::get(class_name).ok_or_else(|| { + Error::Ios( + "GeolocationPlugin class not found. Ensure the Swift package is built and linked." + .to_string(), + ) + })?; + + let instance: *mut objc2::runtime::Object = msg_send![class, alloc]; + let instance: *mut objc2::runtime::Object = msg_send![instance, init]; + Ok::<*mut objc2::runtime::Object, Error>(instance) + })?; + + Ok(&mut **ptr_ref) + } + } + + /// Get current position + pub fn get_current_position(&self, options: Option) -> Result { + let options = options.unwrap_or_default(); + let mtm = + MainThreadMarker::new().ok_or_else(|| Error::Ios("Not on main thread".to_string()))?; + + let plugin = self.get_plugin_instance(mtm)?; + + // Serialize options to JSON + let options_json = serde_json::to_string(&options).map_err(|e| Error::Json(e))?; + + unsafe { + // Create NSString from JSON using NSString::stringWithUTF8String: (class method) + let json_cstr = CString::new(options_json) + .map_err(|e| Error::Ios(format!("Invalid JSON string: {}", e)))?; + let nsstring_class = + objc2::runtime::Class::get(CStr::from_bytes_with_nul(b"NSString\0").unwrap()) + .ok_or_else(|| Error::Ios("NSString class not found".to_string()))?; + let json_nsstring: *mut objc2::runtime::Object = + msg_send![nsstring_class, stringWithUTF8String: json_cstr.as_ptr()]; + + // Call getCurrentPositionJson: on the plugin + let result: *mut objc2::runtime::Object = msg_send![ + plugin, + getCurrentPositionJson: json_nsstring + ]; + + // Convert NSString to Rust String using UTF8String method + let result_cstr: *const c_char = msg_send![&mut *result, UTF8String]; + let result_str = CStr::from_ptr(result_cstr) + .to_str() + .map_err(|e| Error::Ios(format!("Invalid UTF-8 in result: {}", e)))?; + + // Deserialize JSON to Position + let position: Position = + serde_json::from_str(result_str).map_err(|e| Error::Json(e))?; + + Ok(position) + } + } + + /// Check permissions + pub fn check_permissions(&self) -> Result { + let mtm = + MainThreadMarker::new().ok_or_else(|| Error::Ios("Not on main thread".to_string()))?; + + let plugin = self.get_plugin_instance(mtm)?; + + unsafe { + // Call checkPermissionsJson on the plugin + let result: *mut objc2::runtime::Object = msg_send![plugin, checkPermissionsJson]; + + // Convert NSString to Rust String + let result_cstr: *const c_char = msg_send![&mut *result, UTF8String]; + let result_str = CStr::from_ptr(result_cstr) + .to_str() + .map_err(|e| Error::Ios(format!("Invalid UTF-8 in result: {}", e)))?; + let status: PermissionStatus = + serde_json::from_str(result_str).map_err(|e| Error::Json(e))?; + + Ok(status) + } + } + + /// Request permissions + pub fn request_permissions( + &self, + permissions: Option>, + ) -> Result { + let mtm = + MainThreadMarker::new().ok_or_else(|| Error::Ios("Not on main thread".to_string()))?; + + let plugin = self.get_plugin_instance(mtm)?; + + // Serialize permissions to JSON + let perms_json = serde_json::to_string(&permissions).map_err(|e| Error::Json(e))?; + + unsafe { + // Create NSString from JSON + let json_cstr = CString::new(perms_json) + .map_err(|e| Error::Ios(format!("Invalid JSON string: {}", e)))?; + let nsstring_class = + objc2::runtime::Class::get(CStr::from_bytes_with_nul(b"NSString\0").unwrap()) + .ok_or_else(|| Error::Ios("NSString class not found".to_string()))?; + let json_nsstring: *mut objc2::runtime::Object = + msg_send![nsstring_class, stringWithUTF8String: json_cstr.as_ptr()]; + + // Call requestPermissionsJson: on the plugin + let result: *mut objc2::runtime::Object = msg_send![ + plugin, + requestPermissionsJson: json_nsstring + ]; + + // Convert NSString to Rust String + let result_cstr: *const c_char = msg_send![&mut *result, UTF8String]; + let result_str = CStr::from_ptr(result_cstr) + .to_str() + .map_err(|e| Error::Ios(format!("Invalid UTF-8 in result: {}", e)))?; + let status: PermissionStatus = + serde_json::from_str(result_str).map_err(|e| Error::Json(e))?; + + Ok(status) + } + } +} + +impl Default for Geolocation { + fn default() -> Self { + Self::new() + } +} diff --git a/packages/geolocation/src/lib.rs b/packages/geolocation/src/lib.rs new file mode 100644 index 0000000000..194ff3cd60 --- /dev/null +++ b/packages/geolocation/src/lib.rs @@ -0,0 +1,178 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Dioxus Geolocation Plugin +//! +//! This plugin provides APIs for getting and tracking the device's current position +//! on Android and iOS mobile platforms. + +pub use models::*; + +#[cfg(target_os = "android")] +mod android; +#[cfg(target_os = "ios")] +mod ios; + +mod error; +mod models; + +#[cfg(any(target_os = "android", target_os = "ios"))] +mod permissions; + +// Declare Android artifacts for automatic bundling +#[cfg(all(feature = "metadata", target_os = "android"))] +dioxus_platform_bridge::android_plugin!( + plugin = "geolocation", + aar = { env = "DIOXUS_ANDROID_ARTIFACT" }, + deps = ["implementation(\"com.google.android.gms:play-services-location:21.3.0\")"] +); + +// Declare iOS/macOS Swift sources for automatic bundling +#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))] +dioxus_platform_bridge::ios_plugin!( + plugin = "geolocation", + spm = { path = "ios", product = "GeolocationPlugin" } +); + +pub use error::{Error, Result}; + +#[cfg(target_os = "android")] +use android::Geolocation as PlatformGeolocation; +#[cfg(target_os = "ios")] +use ios::Geolocation as PlatformGeolocation; + +/// Access to the geolocation APIs. +/// +/// This struct provides a unified interface for accessing geolocation functionality +/// on both Android and iOS platforms. It automatically initializes and manages the +/// platform-specific implementations. +/// +/// # Example +/// +/// ```rust,no_run +/// use dioxus_geolocation::{Geolocation, PositionOptions}; +/// +/// let mut geolocation = Geolocation::new(); +/// +/// // Check permissions +/// let status = geolocation.check_permissions()?; +/// if status.location == PermissionState::Prompt { +/// let new_status = geolocation.request_permissions(None)?; +/// } +/// +/// // Get current position +/// let options = PositionOptions { +/// enable_high_accuracy: true, +/// timeout: 10000, +/// maximum_age: 0, +/// }; +/// let position = geolocation.get_current_position(Some(options))?; +/// println!("Latitude: {}, Longitude: {}", position.coords.latitude, position.coords.longitude); +/// +/// # Ok::<(), dioxus_geolocation::Error>(()) +/// ``` +pub struct Geolocation { + #[cfg(target_os = "android")] + inner: android::Geolocation, + #[cfg(target_os = "ios")] + inner: ios::Geolocation, +} + +impl Geolocation { + /// Create a new Geolocation instance + pub fn new() -> Self { + Self { + #[cfg(target_os = "android")] + inner: android::Geolocation::new(), + #[cfg(target_os = "ios")] + inner: ios::Geolocation::new(), + } + } + + /// Get the device's current position. + /// + /// # Arguments + /// + /// * `options` - Optional position options. If `None`, default options are used. + /// + /// # Returns + /// + /// Returns the current position or an error if the location cannot be obtained. + pub fn get_current_position(&mut self, options: Option) -> Result { + #[cfg(target_os = "android")] + { + self.inner.get_current_position(options) + } + #[cfg(target_os = "ios")] + { + (&self.inner).get_current_position(options) + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let _ = options; + Err(Error::PlatformBridge( + "Geolocation is only supported on Android and iOS".to_string(), + )) + } + } + + /// Check the current permission status. + /// + /// # Returns + /// + /// Returns the permission status for location and coarse location permissions. + pub fn check_permissions(&mut self) -> Result { + #[cfg(target_os = "android")] + { + self.inner.check_permissions() + } + #[cfg(target_os = "ios")] + { + (&self.inner).check_permissions() + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + Err(Error::PlatformBridge( + "Geolocation is only supported on Android and iOS".to_string(), + )) + } + } + + /// Request location permissions from the user. + /// + /// # Arguments + /// + /// * `permissions` - Optional list of specific permission types to request. + /// If `None`, requests all location permissions. + /// + /// # Returns + /// + /// Returns the permission status after the user responds to the permission request. + pub fn request_permissions( + &mut self, + permissions: Option>, + ) -> Result { + #[cfg(target_os = "android")] + { + self.inner.request_permissions(permissions) + } + #[cfg(target_os = "ios")] + { + (&self.inner).request_permissions(permissions) + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let _ = permissions; + Err(Error::PlatformBridge( + "Geolocation is only supported on Android and iOS".to_string(), + )) + } + } +} + +impl Default for Geolocation { + fn default() -> Self { + Self::new() + } +} diff --git a/packages/geolocation/src/mobile.rs b/packages/geolocation/src/mobile.rs new file mode 100644 index 0000000000..a74887474a --- /dev/null +++ b/packages/geolocation/src/mobile.rs @@ -0,0 +1,62 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{ + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.geolocation"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_geolocation); + +// initializes the Kotlin or Swift plugin classes +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "GeolocationPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_geolocation)?; + Ok(Geolocation(handle)) +} + +/// Access to the geolocation APIs. +pub struct Geolocation(PluginHandle); + +impl Geolocation { + pub fn get_current_position( + &self, + options: Option, + ) -> crate::Result { + // TODO: We may have to send over None if that's better on Android + self.0 + .run_mobile_plugin("getCurrentPosition", options.unwrap_or_default()) + .map_err(Into::into) + } + + pub fn check_permissions(&self) -> crate::Result { + self.0 + .run_mobile_plugin("checkPermissions", ()) + .map_err(Into::into) + } + + pub fn request_permissions( + &self, + permissions: Option>, + ) -> crate::Result { + self.0 + .run_mobile_plugin( + "requestPermissions", + serde_json::json!({ "permissions": permissions }), + ) + .map_err(Into::into) + } +} diff --git a/packages/geolocation/src/models.rs b/packages/geolocation/src/models.rs new file mode 100644 index 0000000000..fddc3abb15 --- /dev/null +++ b/packages/geolocation/src/models.rs @@ -0,0 +1,98 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; + +/// Permission state for geolocation permissions +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[derive(Default)] +pub enum PermissionState { + /// Permission granted + Granted, + /// Permission denied + Denied, + /// Permission not yet determined (user hasn't been asked) + #[default] + Prompt, + /// Permission prompt shown with rationale (Android 12+) + PromptWithRationale, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionStatus { + /// Permission state for the location alias. + /// + /// On Android it requests/checks both ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION permissions. + /// + /// On iOS it requests/checks location permissions. + pub location: PermissionState, + /// Permissions state for the coarseLocation alias. + /// + /// On Android it requests/checks ACCESS_COARSE_LOCATION. + /// + /// On Android 12+, users can choose between Approximate location (ACCESS_COARSE_LOCATION) and Precise location (ACCESS_FINE_LOCATION). + /// + /// On iOS it will have the same value as the `location` alias. + pub coarse_location: PermissionState, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PositionOptions { + /// High accuracy mode (such as GPS, if available) + /// Will be ignored on Android 12+ if users didn't grant the ACCESS_FINE_LOCATION permission. + pub enable_high_accuracy: bool, + /// The maximum wait time in milliseconds for location updates. + /// Default: 10000 + /// On Android the timeout gets ignored for getCurrentPosition. + /// Ignored on iOS. + // TODO: Handle Infinity and default to it. + // TODO: Should be u64+ but specta doesn't like that? + pub timeout: u32, + /// The maximum age in milliseconds of a possible cached position that is acceptable to return. + /// Default: 0 + /// Ignored on iOS. + // TODO: Handle Infinity. + // TODO: Should be u64+ but specta doesn't like that? + pub maximum_age: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PermissionType { + Location, + CoarseLocation, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Coordinates { + /// Latitude in decimal degrees. + pub latitude: f64, + /// Longitude in decimal degrees. + pub longitude: f64, + /// Accuracy level of the latitude and longitude coordinates in meters. + pub accuracy: f64, + /// Accuracy level of the altitude coordinate in meters, if available. + /// Available on all iOS versions and on Android 8 and above. + pub altitude_accuracy: Option, + /// The altitude the user is at, if available. + pub altitude: Option, + // The speed the user is traveling, if available. + pub speed: Option, + /// The heading the user is facing, if available. + pub heading: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Position { + /// Creation time for these coordinates. + // TODO: Check if we're actually losing precision. + pub timestamp: u64, + /// The GPS coordinates along with the accuracy of the data. + pub coords: Coordinates, +} diff --git a/packages/geolocation/src/permissions.rs b/packages/geolocation/src/permissions.rs new file mode 100644 index 0000000000..2bec0653ff --- /dev/null +++ b/packages/geolocation/src/permissions.rs @@ -0,0 +1,31 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Geolocation permissions declaration using Dioxus permissions system +//! +//! This module declares the permissions required for geolocation functionality. +//! These permissions are embedded in the binary and can be extracted by build tools +//! to inject into platform-specific configuration files. + +use permissions::{static_permission, LocationPrecision, Permission, PermissionBuilder}; + +/// Fine location permission +/// +/// This permission allows the app to access precise location data using GPS. +/// On Android, this corresponds to `ACCESS_FINE_LOCATION`. +/// On iOS, this corresponds to `NSLocationWhenInUseUsageDescription`. +pub const FINE_LOCATION: Permission = + static_permission!(PermissionBuilder::location(LocationPrecision::Fine) + .with_description("Access your precise location to provide location-based services") + .build()); + +/// Coarse location permission +/// +/// This permission allows the app to access approximate location data. +/// On Android, this corresponds to `ACCESS_COARSE_LOCATION`. +/// On iOS, this corresponds to `NSLocationWhenInUseUsageDescription`. +pub const COARSE_LOCATION: Permission = + static_permission!(PermissionBuilder::location(LocationPrecision::Coarse) + .with_description("Access your approximate location to provide location-based services") + .build()); diff --git a/packages/manganis/manganis-core/src/asset.rs b/packages/manganis/manganis-core/src/asset.rs index 92c543599a..c1922c31c5 100644 --- a/packages/manganis/manganis-core/src/asset.rs +++ b/packages/manganis/manganis-core/src/asset.rs @@ -133,15 +133,26 @@ impl Asset { if ptr.is_null() { panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker"); } - let mut bytes = ConstVec::new(); + // Use a 4096-byte buffer to accommodate both old format (1024 bytes) and new format (4096 bytes) + let mut bytes = ConstVec::::new_with_max_size(); for byte in 0..len { // SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and // since we are reading a u8 there are no alignment requirements let byte = unsafe { std::ptr::read_volatile(ptr.add(byte)) }; bytes = bytes.push(byte); } - let read = bytes.read(); - deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1 + let read = bytes.as_ref(); + // Try to deserialize as BundledAsset directly + if let Some((_, asset)) = deserialize_const!(BundledAsset, read) { + return asset; + } + // If that fails, the data might still be in SymbolData::Asset format (not processed by CLI yet) + // We can't deserialize SymbolData here due to circular dependency, so provide a helpful error + panic!( + "Failed to deserialize asset. The asset data may be in SymbolData format and needs to be processed by the Dioxus CLI. \ + Make sure you are running 'dx serve' or 'dx build' to process assets. \ + If the error persists, try cleaning your build directory with 'cargo clean' and rebuilding." + ) } /// Return a canonicalized path to the asset diff --git a/packages/manganis/manganis-core/src/options.rs b/packages/manganis/manganis-core/src/options.rs index dd383ab4d8..bed2cf4651 100644 --- a/packages/manganis/manganis-core/src/options.rs +++ b/packages/manganis/manganis-core/src/options.rs @@ -107,7 +107,7 @@ impl AssetOptionsBuilder<()> { impl AssetOptionsBuilder { /// Create a new asset options builder with the given variant - pub(crate) const fn variant(variant: T) -> Self { + pub const fn variant(variant: T) -> Self { Self { add_hash: true, variant, diff --git a/packages/manganis/manganis-macro/Cargo.toml b/packages/manganis/manganis-macro/Cargo.toml index 7371141b90..eba84a9e09 100644 --- a/packages/manganis/manganis-macro/Cargo.toml +++ b/packages/manganis/manganis-macro/Cargo.toml @@ -18,6 +18,7 @@ proc-macro = true proc-macro2 = { workspace = true, features = ["span-locations"] } quote = { workspace = true } syn = { workspace = true, features = ["full", "extra-traits"] } +dx-macro-helpers = { workspace = true } manganis-core = { workspace = true } dunce = { workspace = true } macro-string = { workspace = true } diff --git a/packages/manganis/manganis-macro/src/css_module.rs b/packages/manganis/manganis-macro/src/css_module.rs index 6484811b00..dcbfb89d5c 100644 --- a/packages/manganis/manganis-macro/src/css_module.rs +++ b/packages/manganis/manganis-macro/src/css_module.rs @@ -97,7 +97,7 @@ impl ToTokens for CssModuleParser { let ident = Ident::new(&as_snake, Span::call_site()); values.push(quote! { - pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::const_serialize::ConstStr::new(#id).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() }; + pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::ConstStr::new(#id).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() }; }); } @@ -111,7 +111,7 @@ impl ToTokens for CssModuleParser { let ident = Ident::new(&as_snake, Span::call_site()); values.push(quote! { - pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::const_serialize::ConstStr::new(#class).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() }; + pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::ConstStr::new(#class).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() }; }); } @@ -130,7 +130,7 @@ impl ToTokens for CssModuleParser { // Get the hash to use when builidng hashed css idents. const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options(); - pub(super) const __ASSET_HASH: manganis::macro_helpers::const_serialize::ConstStr = manganis::macro_helpers::hash_asset(&__ASSET_OPTIONS, #hash); + pub(super) const __ASSET_HASH: manganis::macro_helpers::ConstStr = manganis::macro_helpers::hash_asset(&__ASSET_OPTIONS, #hash); // Css ident class for deref stylesheet inclusion. pub(super) struct __CssIdent { pub inner: &'static str } diff --git a/packages/manganis/manganis-macro/src/linker.rs b/packages/manganis/manganis-macro/src/linker.rs index 116d0c63b2..a3c8f117cd 100644 --- a/packages/manganis/manganis-macro/src/linker.rs +++ b/packages/manganis/manganis-macro/src/linker.rs @@ -1,25 +1,19 @@ use proc_macro2::TokenStream as TokenStream2; -use quote::ToTokens; +use quote::{quote, ToTokens}; -/// We store description of the assets an application uses in the executable. -/// We use the `link_section` attribute embed an extra section in the executable. -/// We force rust to store a serialized representation of the asset description -/// inside a particular region of the binary, with the label "manganis". -/// After linking, the "manganis" sections of the different object files will be merged. +/// Generate a linker section for embedding asset data in the binary +/// +/// This function creates a static array containing the serialized asset data +/// and exports it with the __ASSETS__ prefix for unified symbol collection. +/// Uses the generic linker helper from dx-macro-helpers for consistency. pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 { - let position = proc_macro2::Span::call_site(); - let export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position); - - quote::quote! { - // First serialize the asset into a constant sized buffer - const __BUFFER: manganis::macro_helpers::const_serialize::ConstVec = manganis::macro_helpers::serialize_asset(&#asset); - // Then pull out the byte slice - const __BYTES: &[u8] = __BUFFER.as_ref(); - // And the length of the byte slice - const __LEN: usize = __BYTES.len(); - - // Now that we have the size of the asset, copy the bytes into a static array - #[unsafe(export_name = #export_name)] - static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES); - } + dx_macro_helpers::linker::generate_link_section( + asset, + asset_hash, + "__ASSETS__", + quote! { manganis::macro_helpers::serialize_asset }, + quote! { manganis::macro_helpers::copy_bytes }, + quote! { manganis::macro_helpers::ConstVec }, + false, // assets don't need #[used] attribute + ) } diff --git a/packages/manganis/manganis/Cargo.toml b/packages/manganis/manganis/Cargo.toml index 306831d05d..bda47aa4d9 100644 --- a/packages/manganis/manganis/Cargo.toml +++ b/packages/manganis/manganis/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["assets"] [dependencies] const-serialize = { workspace = true } +dx-macro-helpers = { workspace = true } manganis-core = { workspace = true } manganis-macro = { workspace = true } diff --git a/packages/manganis/manganis/src/macro_helpers.rs b/packages/manganis/manganis/src/macro_helpers.rs index 984461b031..e947eeb7fd 100644 --- a/packages/manganis/manganis/src/macro_helpers.rs +++ b/packages/manganis/manganis/src/macro_helpers.rs @@ -1,5 +1,7 @@ -pub use const_serialize; -use const_serialize::{serialize_const, ConstVec, SerializeConst}; +// Re-export const_serialize types for convenience +pub use const_serialize::{self, ConstStr, ConstVec, SerializeConst}; +// Re-export copy_bytes so generated code can use it without dx-macro-helpers dependency +pub use dx_macro_helpers::copy_bytes; use manganis_core::{AssetOptions, BundledAsset}; const PLACEHOLDER_HASH: &str = "This should be replaced by dx as part of the build process. If you see this error, make sure you are using a matching version of dx and dioxus and you are not stripping symbols from your binary."; @@ -23,11 +25,19 @@ pub const fn create_bundled_asset_relative( } /// Serialize an asset to a const buffer -pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec { - let data = ConstVec::new(); - let mut data = serialize_const(asset, data); - // Reserve the maximum size of the asset - while data.len() < BundledAsset::MEMORY_LAYOUT.size() { +/// +/// Serializes the asset directly (not wrapped in SymbolData) for simplicity. +/// Uses a 4096-byte buffer to accommodate assets with large data. +/// The buffer is padded to the full buffer size (4096) to match the +/// linker section size. const-serialize deserialization will ignore +/// the padding (zeros) at the end. +pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec { + // Serialize using the default buffer, then expand into the fixed-size buffer + let serialized = const_serialize::serialize_const(asset, ConstVec::new()); + let mut data: ConstVec = ConstVec::new_with_max_size(); + data = data.extend(serialized.as_ref()); + // Pad to full buffer size (4096) to match linker section size + while data.len() < 4096 { data = data.push(0); } data @@ -36,19 +46,8 @@ pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec { /// Deserialize a const buffer into a BundledAsset pub const fn deserialize_asset(bytes: &[u8]) -> BundledAsset { let bytes = ConstVec::new().extend(bytes); - match const_serialize::deserialize_const!(BundledAsset, bytes.read()) { + match const_serialize::deserialize_const!(BundledAsset, bytes.as_ref()) { Some((_, asset)) => asset, None => panic!("Failed to deserialize asset. This may be caused by a mismatch between your dioxus and dioxus-cli versions"), } } - -/// Copy a slice into a constant sized buffer at compile time -pub const fn copy_bytes(bytes: &[u8]) -> [u8; N] { - let mut out = [0; N]; - let mut i = 0; - while i < N { - out[i] = bytes[i]; - i += 1; - } - out -} diff --git a/packages/mobile-plugin-build/Cargo.toml b/packages/mobile-plugin-build/Cargo.toml new file mode 100644 index 0000000000..20af63a196 --- /dev/null +++ b/packages/mobile-plugin-build/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dioxus-mobile-plugin-build" +version = { workspace = true } +edition = "2021" +license = "MIT OR Apache-2.0" +description = "Build-script helpers for compiling Dioxus mobile plugins" +repository = "https://github.com/DioxusLabs/dioxus" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/dioxus-mobile-plugin-build" + +[dependencies] diff --git a/packages/mobile-plugin-build/src/lib.rs b/packages/mobile-plugin-build/src/lib.rs new file mode 100644 index 0000000000..48faa1f4e7 --- /dev/null +++ b/packages/mobile-plugin-build/src/lib.rs @@ -0,0 +1,373 @@ +//! Build-script helpers for Dioxus mobile plugins. +//! +//! This crate centralizes the shared Gradle (Android) and Swift Package (iOS) +//! build steps the plugins need so that each plugin crate can keep its +//! `build.rs` minimal. + +use std::{ + env, + error::Error, + fs, + path::{Path, PathBuf}, + process::Command, +}; + +/// Result alias used throughout the helper functions. +pub type Result = std::result::Result>; + +/// Configuration for compiling and linking a Swift package for iOS targets. +pub struct SwiftPackageConfig<'a> { + /// Name of the Swift product to build (must match the Package.swift product). + pub product: &'a str, + /// Minimum iOS version string, e.g. `"13.0"`. + pub min_ios_version: &'a str, + /// Absolute path to the Swift package directory (containing Package.swift). + pub package_dir: &'a Path, + /// Additional frameworks to link (passed as `cargo:rustc-link-lib=framework=...`). + pub link_frameworks: &'a [&'a str], + /// Extra static/dynamic libraries to link (passed as `cargo:rustc-link-lib=...`). + pub link_libraries: &'a [&'a str], +} + +/// Build the configured Swift package when targeting iOS and emit the linker +/// configuration required for Cargo to consume the produced static library. +pub fn build_swift_package(config: &SwiftPackageConfig<'_>) -> Result<()> { + let target = env::var("TARGET")?; + if !target.contains("apple-ios") { + return Ok(()); + } + + let (swift_target, sdk_name) = swift_target_and_sdk(&target, config.min_ios_version) + .ok_or_else(|| format!("Unsupported iOS target `{target}` for Swift compilation"))?; + let sdk_path = lookup_sdk_path(sdk_name)?; + + let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".into()); + let configuration = if profile == "release" { + "release" + } else { + "debug" + }; + + let build_dir = PathBuf::from(env::var("OUT_DIR")?).join("swift-build"); + + let status = Command::new("xcrun") + .arg("swift") + .arg("build") + .arg("--package-path") + .arg(config.package_dir) + .arg("--configuration") + .arg(configuration) + .arg("--triple") + .arg(&swift_target) + .arg("--sdk") + .arg(&sdk_path) + .arg("--product") + .arg(config.product) + .arg("--build-path") + .arg(&build_dir) + .status()?; + + if !status.success() { + return Err("swift build failed. Check the log above for details.".into()); + } + + let lib_path = find_static_lib(&build_dir, configuration, &swift_target, config.product) + .ok_or_else(|| { + format!( + "Could not locate Swift static library for product `{}`", + config.product + ) + })?; + + if let Some(parent) = lib_path.parent() { + println!("cargo:rustc-link-search=native={}", parent.display()); + } + let runtime_lib_dir = swift_runtime_lib_dir(&swift_target)?; + println!( + "cargo:rustc-link-search=native={}", + runtime_lib_dir.display() + ); + println!("cargo:rustc-link-lib=static={}", config.product); + // Force load the plugin archive so ObjC registries are included. + println!("cargo:rustc-link-arg=-Xlinker"); + println!("cargo:rustc-link-arg=-force_load"); + println!("cargo:rustc-link-arg=-Xlinker"); + println!("cargo:rustc-link-arg={}", lib_path.display()); + println!("cargo:rustc-link-arg=-ObjC"); + + for framework in config.link_frameworks { + println!("cargo:rustc-link-lib=framework={framework}"); + } + for lib in config.link_libraries { + println!("cargo:rustc-link-lib={lib}"); + } + + Ok(()) +} + +/// Configuration shared by Android plugin builds. +pub struct AndroidLibraryConfig<'a> { + /// Absolute path to the Gradle project directory (contains `gradlew`/`build.gradle.kts`). + pub project_dir: &'a Path, + /// Preferred location of the built AAR (relative to the crate root). + pub preferred_artifact: &'a Path, + /// The environment variable name to expose the copied artifact path under. + pub artifact_env_key: &'a str, + /// The Gradle task to run when building (defaults to `assembleRelease` in users). + pub gradle_task: &'a str, +} + +/// Compile the Android library with Gradle when targeting Android and expose the +/// built AAR through the configured environment variable. +pub fn build_android_library(config: &AndroidLibraryConfig<'_>) -> Result<()> { + let target = env::var("TARGET")?; + if !target.contains("android") { + return Ok(()); + } + + let gradle_cmd = resolve_gradle_command(config.project_dir)?; + let java_home = env::var("DX_ANDROID_JAVA_HOME") + .or_else(|_| env::var("ANDROID_JAVA_HOME")) + .or_else(|_| env::var("JAVA_HOME")) + .ok(); + let sdk_root = env::var("DX_ANDROID_SDK_ROOT") + .or_else(|_| env::var("ANDROID_SDK_ROOT")) + .ok(); + let ndk_home = env::var("DX_ANDROID_NDK_HOME") + .or_else(|_| env::var("ANDROID_NDK_HOME")) + .ok(); + + let mut command = Command::new(&gradle_cmd); + command + .arg(config.gradle_task) + .current_dir(config.project_dir); + + if let Some(ref java_home) = java_home { + command.env("JAVA_HOME", java_home); + command.env("DX_ANDROID_JAVA_HOME", java_home); + let mut gradle_opts = env::var("GRADLE_OPTS").unwrap_or_default(); + if !gradle_opts.is_empty() { + gradle_opts.push(' '); + } + gradle_opts.push_str(&format!("-Dorg.gradle.java.home={java_home}")); + command.env("GRADLE_OPTS", gradle_opts); + } + if let Some(ref sdk_root) = sdk_root { + command.env("ANDROID_SDK_ROOT", sdk_root); + command.env("ANDROID_HOME", sdk_root); + command.env("DX_ANDROID_SDK_ROOT", sdk_root); + } + if let Some(ref ndk_home) = ndk_home { + command.env("ANDROID_NDK_HOME", ndk_home); + command.env("NDK_HOME", ndk_home); + command.env("DX_ANDROID_NDK_HOME", ndk_home); + } + + let status = command.status().map_err(|e| { + format!( + "Failed to invoke `{}` while building Android plugin: {e}", + gradle_cmd + ) + })?; + + if !status.success() { + return Err(format!( + "Gradle build failed while compiling Android plugin using `{gradle_cmd}`" + ) + .into()); + } + + let mut aar_path = config.preferred_artifact.to_path_buf(); + if !aar_path.exists() { + aar_path = discover_release_aar(config.project_dir).ok_or_else(|| { + format!( + "Expected Android AAR at `{}` or any '*-release.aar' under `{}`", + config.preferred_artifact.display(), + config.project_dir.join("build/outputs/aar").display() + ) + })?; + } + + let artifact_dir = env::var_os("DX_ANDROID_ARTIFACT_DIR") + .map(PathBuf::from) + .or_else(|| { + env::var_os("OUT_DIR") + .map(PathBuf::from) + .map(|dir| dir.join("android-artifacts")) + }) + .ok_or_else(|| "DX_ANDROID_ARTIFACT_DIR not set and OUT_DIR unavailable".to_string())?; + + fs::create_dir_all(&artifact_dir)?; + let filename = aar_path + .file_name() + .ok_or_else(|| format!("AAR path missing filename: {}", aar_path.display()))?; + let dest_path = artifact_dir.join(filename); + fs::copy(&aar_path, &dest_path)?; + let dest_str = dest_path.to_str().ok_or_else(|| { + format!( + "Artifact path contains non-UTF8 characters: {}", + dest_path.display() + ) + })?; + println!("cargo:rustc-env={}={dest_str}", config.artifact_env_key); + + Ok(()) +} + +fn swift_target_and_sdk(target: &str, min_ios: &str) -> Option<(String, &'static str)> { + if target.starts_with("aarch64-apple-ios-sim") { + Some(( + format!("arm64-apple-ios{min_ios}-simulator"), + "iphonesimulator", + )) + } else if target.starts_with("aarch64-apple-ios") { + Some((format!("arm64-apple-ios{min_ios}"), "iphoneos")) + } else if target.starts_with("x86_64-apple-ios") { + Some(( + format!("x86_64-apple-ios{min_ios}-simulator"), + "iphonesimulator", + )) + } else { + None + } +} + +fn lookup_sdk_path(sdk: &str) -> Result { + let output = Command::new("xcrun") + .arg("--sdk") + .arg(sdk) + .arg("--show-sdk-path") + .output()?; + if output.status.success() { + Ok(String::from_utf8(output.stdout)?.trim().to_string()) + } else { + Err(format!( + "xcrun failed to locate SDK {sdk}: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into()) + } +} + +fn swift_runtime_lib_dir(swift_target: &str) -> Result { + let output = Command::new("xcode-select").arg("-p").output()?; + if !output.status.success() { + return Err(format!( + "xcode-select -p failed: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + let developer_dir = PathBuf::from(String::from_utf8(output.stdout)?.trim()); + let toolchain_dir = developer_dir + .join("Toolchains") + .join("XcodeDefault.xctoolchain") + .join("usr") + .join("lib") + .join("swift"); + + let platform_dir = if swift_target.contains("simulator") { + "iphonesimulator" + } else { + "iphoneos" + }; + + let runtime_dir = toolchain_dir.join(platform_dir); + if runtime_dir.exists() { + Ok(runtime_dir) + } else { + Err(format!( + "Swift runtime library directory not found: {}", + runtime_dir.display() + ) + .into()) + } +} + +fn find_static_lib( + build_dir: &Path, + configuration: &str, + swift_target: &str, + product: &str, +) -> Option { + let lib_name = format!("lib{product}.a"); + let candidates = [ + build_dir + .join(configuration) + .join(swift_target) + .join(&lib_name), + build_dir + .join(swift_target) + .join(configuration) + .join(&lib_name), + build_dir.join(configuration).join(&lib_name), + ]; + + for candidate in candidates { + if candidate.exists() { + return Some(candidate); + } + } + + find_file_recursively(build_dir, &lib_name) +} + +fn find_file_recursively(root: &Path, needle: &str) -> Option { + if !root.exists() { + return None; + } + + for entry in fs::read_dir(root).ok()? { + let entry = entry.ok()?; + let path = entry.path(); + if path.is_file() && path.file_name().is_some_and(|n| n == needle) { + return Some(path); + } + if path.is_dir() { + if let Some(found) = find_file_recursively(&path, needle) { + return Some(found); + } + } + } + + None +} + +fn resolve_gradle_command(project_dir: &Path) -> Result { + if let Ok(cmd) = env::var("GRADLE") { + return Ok(cmd); + } + + let gradlew = project_dir.join("gradlew"); + if gradlew.exists() { + return Ok(gradlew.display().to_string()); + } + + Ok("gradle".to_string()) +} + +fn discover_release_aar(project_dir: &Path) -> Option { + let outputs_dir = project_dir.join("build/outputs/aar"); + if !outputs_dir.exists() { + return None; + } + + fs::read_dir(&outputs_dir) + .ok()? + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path()) + .filter(|path| { + path.is_file() + && path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("aar")) + .unwrap_or(false) + }) + .find(|path| { + path.file_name() + .and_then(|n| n.to_str()) + .map(|name| name.ends_with("-release.aar")) + .unwrap_or(false) + }) +} diff --git a/packages/permissions/permissions-core/Cargo.toml b/packages/permissions/permissions-core/Cargo.toml new file mode 100644 index 0000000000..8f4859cc84 --- /dev/null +++ b/packages/permissions/permissions-core/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "permissions-core" +version = { workspace = true } +edition = "2021" +description = "Core types and platform mappings for the permissions system" +authors = ["DioxusLabs"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/permissions-core" +keywords = ["permissions", "mobile", "desktop", "web"] +categories = ["development-tools::build-utils"] + +[dependencies] +const-serialize = { workspace = true } +const-serialize-macro = { workspace = true } +manganis-core = { workspace = true } +serde = { version = "1.0", features = ["derive"] } + +[dev-dependencies] diff --git a/packages/permissions/permissions-core/src/lib.rs b/packages/permissions/permissions-core/src/lib.rs new file mode 100644 index 0000000000..b98d351b26 --- /dev/null +++ b/packages/permissions/permissions-core/src/lib.rs @@ -0,0 +1,13 @@ +mod permission; +mod platforms; +mod symbol_data; + +pub use permission::*; +pub use platforms::*; +pub use symbol_data::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData}; + +// Re-export PermissionBuilder and CustomPermissionBuilder for convenience +pub use permission::{CustomPermissionBuilder, PermissionBuilder}; + +// Re-export const_serialize types for use in macros +pub use const_serialize::ConstStr; diff --git a/packages/permissions/permissions-core/src/permission.rs b/packages/permissions/permissions-core/src/permission.rs new file mode 100644 index 0000000000..26cf081b76 --- /dev/null +++ b/packages/permissions/permissions-core/src/permission.rs @@ -0,0 +1,478 @@ +use const_serialize::{ConstStr, SerializeConst}; +use std::hash::{Hash, Hasher}; + +use crate::{PermissionKind, Platform, PlatformFlags, PlatformIdentifiers, SymbolData}; + +/// A permission declaration that can be embedded in the binary +/// +/// This struct contains all the information needed to declare a permission +/// across all supported platforms. It uses const-serialize to be embeddable +/// in linker sections. +#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)] +pub struct Permission { + /// The kind of permission being declared + kind: PermissionKind, + /// User-facing description of why this permission is needed + description: ConstStr, + /// Platforms where this permission is supported + supported_platforms: PlatformFlags, +} + +impl Permission { + /// Create a new permission with the given kind and description + pub const fn new(kind: PermissionKind, description: &'static str) -> Self { + let supported_platforms = kind.supported_platforms(); + Self { + kind, + description: ConstStr::new(description), + supported_platforms, + } + } + + /// Get the permission kind + pub const fn kind(&self) -> &PermissionKind { + &self.kind + } + + /// Get the user-facing description + pub fn description(&self) -> &str { + self.description.as_str() + } + + /// Get the platforms that support this permission + pub const fn supported_platforms(&self) -> PlatformFlags { + self.supported_platforms + } + + /// Check if this permission is supported on the given platform + pub const fn supports_platform(&self, platform: Platform) -> bool { + self.supported_platforms.supports(platform) + } + + /// Get the platform-specific identifiers for this permission + pub const fn platform_identifiers(&self) -> PlatformIdentifiers { + self.kind.platform_identifiers() + } + + /// Get the Android permission string, if supported + pub fn android_permission(&self) -> Option { + self.platform_identifiers() + .android + .map(|s| s.as_str().to_string()) + } + + /// Get the iOS/macOS usage description key, if supported + pub fn ios_key(&self) -> Option { + self.platform_identifiers() + .ios + .map(|s| s.as_str().to_string()) + } + + /// Get the macOS usage description key, if supported + pub fn macos_key(&self) -> Option { + self.platform_identifiers() + .macos + .map(|s| s.as_str().to_string()) + } + + /// Deserialize a permission from the bytes emitted into linker sections. + /// This helper mirrors what the CLI performs when it scans the binary and + /// allows runtime consumers to interpret the serialized metadata as well. + pub fn from_embedded(bytes: &[u8]) -> Option { + const SYMBOL_SIZE: usize = std::mem::size_of::(); + let (_, symbol) = + unsafe { const_serialize::deserialize_const_raw::(bytes) }?; + match symbol { + SymbolData::Permission(permission) => Some(permission), + _ => None, + } + } +} + +impl Hash for Permission { + fn hash(&self, state: &mut H) { + self.kind.hash(state); + self.description.hash(state); + self.supported_platforms.hash(state); + } +} + +/// A collection of permissions that can be serialized and embedded +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PermissionManifest { + /// All permissions declared in the application + permissions: Vec, +} + +impl PermissionManifest { + /// Create a new empty permission manifest + pub fn new() -> Self { + Self { + permissions: Vec::new(), + } + } + + /// Create a manifest from an existing list of permissions + pub fn from_permissions(permissions: Vec) -> Self { + Self { permissions } + } + + /// Add a permission to the manifest + pub fn add_permission(&mut self, permission: Permission) { + self.permissions.push(permission); + } + + /// Get all permissions in the manifest + pub fn permissions(&self) -> &[Permission] { + &self.permissions + } + + /// Get permissions for a specific platform + pub fn permissions_for_platform(&self, platform: Platform) -> Vec<&Permission> { + self.permissions + .iter() + .filter(|p| p.supports_platform(platform)) + .collect() + } + + /// Check if the manifest contains any permissions + pub fn is_empty(&self) -> bool { + self.permissions.is_empty() + } + + /// Get the number of permissions in the manifest + pub fn len(&self) -> usize { + self.permissions.len() + } +} + +impl Default for PermissionManifest { + fn default() -> Self { + Self::new() + } +} + +/// Builder for custom permissions with platform-specific identifiers +/// +/// This builder uses named methods to specify platform identifiers, +/// making it clear which value belongs to which platform. +/// +/// # Examples +/// +/// ```rust +/// use permissions_core::{Permission, PermissionBuilder}; +/// +/// const CUSTOM: Permission = PermissionBuilder::custom() +/// .with_android("android.permission.MY_PERMISSION") +/// .with_ios("NSMyUsageDescription") +/// .with_macos("NSMyUsageDescription") +/// .with_description("Custom permission") +/// .build(); +/// ``` +#[derive(Debug, Clone)] +pub struct CustomPermissionBuilder { + android: Option, + ios: Option, + macos: Option, + description: Option, +} + +impl CustomPermissionBuilder { + /// Set the Android permission string. + /// + /// Call this when the permission applies to Android. Omit it for iOS/macOS-only permissions. + /// + /// # Examples + /// ```rust + /// use permissions_core::{Permission, PermissionBuilder}; + /// + /// const PERM: Permission = PermissionBuilder::custom() + /// .with_android("android.permission.READ_EXTERNAL_STORAGE") + /// .with_ios("NSPhotoLibraryUsageDescription") + /// .with_macos("NSPhotoLibraryUsageDescription") + /// .with_description("Access files") + /// .build(); + /// ``` + pub const fn with_android(mut self, android: &'static str) -> Self { + self.android = Some(ConstStr::new(android)); + self + } + + /// Set the iOS usage description key + /// + /// This key is used in the iOS Info.plist file. + /// + /// Call this when the permission applies to iOS. Omit it when not needed. + /// + /// # Examples + /// + /// ```rust + /// use permissions_core::{Permission, PermissionBuilder}; + /// + /// const PERM: Permission = PermissionBuilder::custom() + /// .with_android("android.permission.READ_EXTERNAL_STORAGE") + /// .with_ios("NSPhotoLibraryUsageDescription") + /// .with_macos("NSPhotoLibraryUsageDescription") + /// .with_description("Access files") + /// .build(); + /// ``` + pub const fn with_ios(mut self, ios: &'static str) -> Self { + self.ios = Some(ConstStr::new(ios)); + self + } + + /// Set the macOS usage description key + /// + /// This key is used in the macOS Info.plist file. + /// + /// Call this when the permission applies to macOS. Omit it when not needed. + /// + /// # Examples + /// + /// ```rust + /// use permissions_core::{Permission, PermissionBuilder}; + /// + /// const PERM: Permission = PermissionBuilder::custom() + /// .with_android("android.permission.READ_EXTERNAL_STORAGE") + /// .with_ios("NSPhotoLibraryUsageDescription") + /// .with_macos("NSPhotoLibraryUsageDescription") + /// .with_description("Access files") + /// .build(); + /// ``` + pub const fn with_macos(mut self, macos: &'static str) -> Self { + self.macos = Some(ConstStr::new(macos)); + self + } + + /// Set the user-facing description for this permission + /// + /// This description is used in platform manifests (Info.plist, AndroidManifest.xml) + /// to explain why the permission is needed. + pub const fn with_description(mut self, description: &'static str) -> Self { + self.description = Some(ConstStr::new(description)); + self + } + + /// Build the permission from the builder + /// + /// This validates that all required fields are set, then creates the `Permission` instance. + /// + /// # Panics + /// + /// This method will cause a compile-time error if any required field is missing: + /// - `description` - User-facing description must be set + /// - `android`/`ios`/`macos` - At least one platform identifier must be provided + pub const fn build(self) -> Permission { + let description = match self.description { + Some(d) => d, + None => panic!("CustomPermissionBuilder::build() requires description field to be set. Call .with_description() before .build()"), + }; + + if self.android.is_none() && self.ios.is_none() && self.macos.is_none() { + panic!("CustomPermissionBuilder::build() requires at least one platform identifier. Call .with_android(), .with_ios(), or .with_macos() before .build()"); + } + + let android = match self.android { + Some(value) => value, + None => ConstStr::new(""), + }; + let ios = match self.ios { + Some(value) => value, + None => ConstStr::new(""), + }; + let macos = match self.macos { + Some(value) => value, + None => ConstStr::new(""), + }; + + let kind = PermissionKind::Custom { + android, + ios, + macos, + android_enabled: self.android.is_some(), + ios_enabled: self.ios.is_some(), + macos_enabled: self.macos.is_some(), + }; + let supported_platforms = kind.supported_platforms(); + + Permission { + kind, + description, + supported_platforms, + } + } +} + +/// Builder for creating permissions with a const-friendly API +/// +/// This builder is used for location and custom permissions that require +/// additional configuration. For simple permissions like Camera, Microphone, +/// and Notifications, use `Permission::new()` directly. +/// +/// # Examples +/// +/// ```rust +/// use permissions_core::{Permission, PermissionBuilder, LocationPrecision}; +/// +/// // Location permission with fine precision +/// const LOCATION: Permission = PermissionBuilder::location(LocationPrecision::Fine) +/// .with_description("Track your runs") +/// .build(); +/// +/// // Custom permission +/// const CUSTOM: Permission = PermissionBuilder::custom() +/// .with_android("android.permission.MY_PERMISSION") +/// .with_ios("NSMyUsageDescription") +/// .with_macos("NSMyUsageDescription") +/// .with_description("Custom permission") +/// .build(); +/// ``` +#[derive(Debug, Clone)] +pub struct PermissionBuilder { + /// The permission kind being built + kind: Option, + /// The user-facing description + description: Option, +} + +impl PermissionBuilder { + /// Create a new location permission builder with the specified precision + /// + /// # Examples + /// + /// ```rust + /// use permissions_core::{Permission, PermissionBuilder, LocationPrecision}; + /// + /// const LOCATION: Permission = PermissionBuilder::location(LocationPrecision::Fine) + /// .with_description("Track your runs") + /// .build(); + /// ``` + pub const fn location(precision: crate::LocationPrecision) -> Self { + Self { + kind: Some(PermissionKind::Location(precision)), + description: None, + } + } + + /// Start building a custom permission with platform-specific identifiers + /// + /// Use the chained methods to specify each platform's identifier: + /// - `.with_android()` - Android permission string + /// - `.with_ios()` - iOS usage description key + /// - `.with_macos()` - macOS usage description key + /// + /// # Examples + /// + /// ```rust + /// use permissions_core::{Permission, PermissionBuilder}; + /// + /// // Custom permission with all platforms + /// const CUSTOM: Permission = PermissionBuilder::custom() + /// .with_android("android.permission.MY_PERMISSION") + /// .with_ios("NSMyUsageDescription") + /// .with_macos("NSMyUsageDescription") + /// .with_description("Custom permission") + /// .build(); + /// + /// // Custom permission where iOS and macOS use the same key + /// const PHOTO_LIBRARY: Permission = PermissionBuilder::custom() + /// .with_android("android.permission.READ_EXTERNAL_STORAGE") + /// .with_ios("NSPhotoLibraryUsageDescription") + /// .with_macos("NSPhotoLibraryUsageDescription") + /// .with_description("Access your photo library") + /// .build(); + /// ``` + pub const fn custom() -> CustomPermissionBuilder { + CustomPermissionBuilder { + android: None, + ios: None, + macos: None, + description: None, + } + } + + /// Set the user-facing description for this permission + /// + /// This description is used in platform manifests (Info.plist, AndroidManifest.xml) + /// to explain why the permission is needed. + /// + /// # Examples + /// + /// ```rust + /// use permissions_core::{Permission, PermissionBuilder, LocationPrecision}; + /// + /// const LOCATION: Permission = PermissionBuilder::location(LocationPrecision::Fine) + /// .with_description("Track your runs") + /// .build(); + /// ``` + pub const fn with_description(mut self, description: &'static str) -> Self { + self.description = Some(ConstStr::new(description)); + self + } + + /// Build the permission from the builder + /// + /// This validates that both the kind and description are set, then creates + /// the `Permission` instance. + /// + /// # Panics + /// + /// This method will cause a compile-time error if any required field is missing: + /// - `kind` - Permission kind must be set by calling `.location()` or `.custom()` before `.build()` + /// - `description` - User-facing description must be set by calling `.with_description()` before `.build()` + pub const fn build(self) -> Permission { + let kind = match self.kind { + Some(k) => k, + None => panic!("PermissionBuilder::build() requires permission kind to be set. Call .location() or .custom() before .build()"), + }; + + let description = match self.description { + Some(d) => d, + None => panic!("PermissionBuilder::build() requires description field to be set. Call .with_description() before .build()"), + }; + + let supported_platforms = kind.supported_platforms(); + Permission { + kind, + description, + supported_platforms, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use const_serialize::{serialize_const, ConstVec}; + + #[test] + fn custom_permission_with_partial_platforms() { + let permission = PermissionBuilder::custom() + .with_android("android.permission.CAMERA") + .with_description("Camera access on Android") + .build(); + + assert!(permission.supports_platform(Platform::Android)); + assert!(!permission.supports_platform(Platform::Ios)); + assert!(!permission.supports_platform(Platform::Macos)); + } + + #[test] + #[should_panic( + expected = "CustomPermissionBuilder::build() requires at least one platform identifier" + )] + fn custom_permission_requires_platform() { + let _ = PermissionBuilder::custom() + .with_description("Missing identifiers") + .build(); + } + + #[test] + fn deserialize_permission_from_embedded_bytes() { + let permission = Permission::new(PermissionKind::Camera, "Camera access"); + let buffer = serialize_const(&SymbolData::Permission(permission), ConstVec::::new()); + let decoded = Permission::from_embedded(buffer.as_ref()).expect("permission decoded"); + assert_eq!(decoded.description(), permission.description()); + assert!(decoded.supports_platform(Platform::Android)); + } +} diff --git a/packages/permissions/permissions-core/src/platforms.rs b/packages/permissions/permissions-core/src/platforms.rs new file mode 100644 index 0000000000..26aac4a527 --- /dev/null +++ b/packages/permissions/permissions-core/src/platforms.rs @@ -0,0 +1,161 @@ +use const_serialize::{ConstStr, SerializeConst}; + +/// Platform categories for permission mapping +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)] +pub enum Platform { + /// Mobile platforms + Android, + Ios, + /// Desktop Darwin platform + Macos, +} + +/// Bit flags for supported platforms +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)] +pub struct PlatformFlags(u8); + +impl PlatformFlags { + pub const fn new() -> Self { + Self(0) + } +} + +impl Default for PlatformFlags { + fn default() -> Self { + Self::new() + } +} + +impl PlatformFlags { + pub const fn with_platform(mut self, platform: Platform) -> Self { + self.0 |= 1 << platform as u8; + self + } + + pub const fn supports(&self, platform: Platform) -> bool { + (self.0 & (1 << platform as u8)) != 0 + } + + pub const fn all() -> Self { + Self(0b000111) // Android + iOS + macOS + } + + pub const fn mobile() -> Self { + Self(0b000011) // Android + iOS + } +} + +/// Location precision for location-based permissions +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)] +pub enum LocationPrecision { + /// Fine location (GPS-level accuracy) + Fine, + /// Coarse location (network-based accuracy) + Coarse, +} + +/// Core permission kinds that map to platform-specific requirements +/// +/// Only tested and verified permissions are included. For untested permissions, +/// use the `Custom` variant with platform-specific identifiers. +#[repr(C, u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)] +#[allow(clippy::large_enum_variant)] // Custom variant contains large ConstStr fields needed for const serialization +pub enum PermissionKind { + /// Camera access + Camera, + /// Location access with precision + Location(LocationPrecision), + /// Microphone access + Microphone, + /// Push notifications + Notifications, + /// Custom permission with platform-specific identifiers + Custom { + android: ConstStr, + ios: ConstStr, + macos: ConstStr, + android_enabled: bool, + ios_enabled: bool, + macos_enabled: bool, + }, +} + +impl PermissionKind { + /// Get the platform-specific permission identifiers for this permission kind + pub const fn platform_identifiers(&self) -> PlatformIdentifiers { + match self { + PermissionKind::Camera => PlatformIdentifiers { + android: Some(ConstStr::new("android.permission.CAMERA")), + ios: Some(ConstStr::new("NSCameraUsageDescription")), + macos: Some(ConstStr::new("NSCameraUsageDescription")), + }, + PermissionKind::Location(LocationPrecision::Fine) => PlatformIdentifiers { + android: Some(ConstStr::new("android.permission.ACCESS_FINE_LOCATION")), + ios: Some(ConstStr::new( + "NSLocationAlwaysAndWhenInUseUsageDescription", + )), + macos: Some(ConstStr::new("NSLocationUsageDescription")), + }, + PermissionKind::Location(LocationPrecision::Coarse) => PlatformIdentifiers { + android: Some(ConstStr::new("android.permission.ACCESS_COARSE_LOCATION")), + ios: Some(ConstStr::new("NSLocationWhenInUseUsageDescription")), + macos: Some(ConstStr::new("NSLocationUsageDescription")), + }, + PermissionKind::Microphone => PlatformIdentifiers { + android: Some(ConstStr::new("android.permission.RECORD_AUDIO")), + ios: Some(ConstStr::new("NSMicrophoneUsageDescription")), + macos: Some(ConstStr::new("NSMicrophoneUsageDescription")), + }, + PermissionKind::Notifications => PlatformIdentifiers { + android: Some(ConstStr::new("android.permission.POST_NOTIFICATIONS")), + ios: None, // Runtime request only + macos: None, // Runtime request only + }, + PermissionKind::Custom { + android, + ios, + macos, + android_enabled, + ios_enabled, + macos_enabled, + } => PlatformIdentifiers { + android: if *android_enabled { + Some(*android) + } else { + None + }, + ios: if *ios_enabled { Some(*ios) } else { None }, + macos: if *macos_enabled { Some(*macos) } else { None }, + }, + } + } + + /// Get the platforms that support this permission kind + pub const fn supported_platforms(&self) -> PlatformFlags { + let identifiers = self.platform_identifiers(); + let mut flags = PlatformFlags::new(); + + if identifiers.android.is_some() { + flags = flags.with_platform(Platform::Android); + } + if identifiers.ios.is_some() { + flags = flags.with_platform(Platform::Ios); + } + if identifiers.macos.is_some() { + flags = flags.with_platform(Platform::Macos); + } + + flags + } +} + +/// Platform-specific permission identifiers +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlatformIdentifiers { + pub android: Option, + pub ios: Option, + pub macos: Option, +} diff --git a/packages/permissions/permissions-core/src/symbol_data.rs b/packages/permissions/permissions-core/src/symbol_data.rs new file mode 100644 index 0000000000..f23677c660 --- /dev/null +++ b/packages/permissions/permissions-core/src/symbol_data.rs @@ -0,0 +1,69 @@ +use const_serialize::{ConstStr, SerializeConst}; +use manganis_core::BundledAsset; + +use crate::Permission; + +/// Unified symbol data that can represent both assets and permissions +/// +/// This enum is used to serialize different types of metadata into the binary +/// using the same `__ASSETS__` symbol prefix. The CBOR format allows for +/// self-describing data, making it easy to add new variants in the future. +/// +/// Variant order does NOT matter for CBOR enum serialization - variants are +/// matched by name (string), not by position or tag value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)] +#[repr(C, u8)] +pub enum SymbolData { + /// An asset that should be bundled with the application + Asset(BundledAsset), + /// A permission declaration for the application + Permission(Permission), + /// Android plugin metadata (prebuilt artifacts + Gradle deps) + AndroidArtifact(AndroidArtifactMetadata), + /// Swift package metadata (SPM location + product) + SwiftPackage(SwiftPackageMetadata), +} + +/// Metadata describing an Android plugin artifact (.aar) that must be copied into the host Gradle project. +#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)] +pub struct AndroidArtifactMetadata { + pub plugin_name: ConstStr, + pub artifact_path: ConstStr, + pub gradle_dependencies: ConstStr, +} + +impl AndroidArtifactMetadata { + pub const fn new( + plugin_name: &'static str, + artifact_path: &'static str, + gradle_dependencies: &'static str, + ) -> Self { + Self { + plugin_name: ConstStr::new(plugin_name), + artifact_path: ConstStr::new(artifact_path), + gradle_dependencies: ConstStr::new(gradle_dependencies), + } + } +} + +/// Metadata for a Swift package that needs to be linked into the app (iOS/macOS). +#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)] +pub struct SwiftPackageMetadata { + pub plugin_name: ConstStr, + pub package_path: ConstStr, + pub product: ConstStr, +} + +impl SwiftPackageMetadata { + pub const fn new( + plugin_name: &'static str, + package_path: &'static str, + product: &'static str, + ) -> Self { + Self { + plugin_name: ConstStr::new(plugin_name), + package_path: ConstStr::new(package_path), + product: ConstStr::new(product), + } + } +} diff --git a/packages/permissions/permissions-macro/Cargo.toml b/packages/permissions/permissions-macro/Cargo.toml new file mode 100644 index 0000000000..1f0088f0b3 --- /dev/null +++ b/packages/permissions/permissions-macro/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "permissions-macro" +version = { workspace = true } +edition = "2021" +description = "Procedural macro for declaring permissions with linker embedding" +authors = ["DioxusLabs"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/permissions-macro" +keywords = ["permissions", "macro", "linker"] +categories = ["development-tools::procedural-macro-helpers"] + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" +dx-macro-helpers = { workspace = true } +permissions-core = { workspace = true } +const-serialize = { workspace = true } + +[dev-dependencies] diff --git a/packages/permissions/permissions-macro/README.md b/packages/permissions/permissions-macro/README.md new file mode 100644 index 0000000000..79e9b020f2 --- /dev/null +++ b/packages/permissions/permissions-macro/README.md @@ -0,0 +1,83 @@ +# Permissions Macro + +Procedural macro for declaring permissions with linker embedding. + +This crate provides the `permission!()` and `static_permission!()` macros that allow you to declare permissions +that will be embedded in the binary using linker sections, similar to how Manganis +embeds assets. Use `static_permission!()` when you want to make it explicit that a +permission is a compile-time (linker) declaration that should be emitted into +platform manifests (Info.plist, AndroidManifest.xml, etc.). The `permission!()` +alias is kept for backward compatibility. + +## Usage + +The macro accepts any expression that evaluates to a `Permission`. There are two patterns: + +### Builder Pattern (for Location and Custom permissions) + +Location and custom permissions use the builder pattern: + +```rust +use permissions_core::{Permission, PermissionBuilder, LocationPrecision}; +use permissions_macro::static_permission; + +// Location permission with fine precision +const LOCATION_FINE: Permission = static_permission!( + PermissionBuilder::location(LocationPrecision::Fine) + .with_description("Track your runs") + .build() +); + +// Location permission with coarse precision +const LOCATION_COARSE: Permission = static_permission!( + PermissionBuilder::location(LocationPrecision::Coarse) + .with_description("Approximate location") + .build() +); + +// Custom permission +const CUSTOM: Permission = static_permission!( + PermissionBuilder::custom() + .with_android("android.permission.MY_PERMISSION") + .with_ios("NSMyUsageDescription") + .with_macos("NSMyUsageDescription") + .with_description("Custom permission") + .build() +); +``` + +### Direct Construction (for simple permissions) + +Simple permissions like Camera, Microphone, and Notifications use direct construction: + +```rust +use permissions_core::{Permission, PermissionKind}; +use permissions_macro::static_permission; + +// Camera permission +const CAMERA: Permission = static_permission!( + Permission::new(PermissionKind::Camera, "Take photos") +); + +// Microphone permission +const MICROPHONE: Permission = static_permission!( + Permission::new(PermissionKind::Microphone, "Record audio") +); + +// Notifications permission +const NOTIFICATIONS: Permission = static_permission!( + Permission::new(PermissionKind::Notifications, "Send notifications") +); +``` + +## How it works + +The macro generates code that: + +1. Creates a `Permission` instance with the specified kind and description +2. Serializes the permission data into a const buffer +3. Embeds the data in a linker section with a unique symbol name (`__PERMISSION__`) +4. Returns a `Permission` that can read the embedded data at runtime + +This allows build tools to extract all permission declarations from the binary +by scanning for `__PERMISSION__*` symbols. diff --git a/packages/permissions/permissions-macro/src/lib.rs b/packages/permissions/permissions-macro/src/lib.rs new file mode 100644 index 0000000000..b98e8b6b5f --- /dev/null +++ b/packages/permissions/permissions-macro/src/lib.rs @@ -0,0 +1,97 @@ +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +pub(crate) mod linker; +pub(crate) mod permission; + +use permission::PermissionParser; + +/// Declare a permission that will be embedded in the binary +/// +/// # Syntax +/// +/// The macro accepts any expression that evaluates to a `Permission`. There are two patterns: +/// +/// ## Builder Pattern (for Location and Custom permissions) +/// +/// Location permissions use the builder pattern: +/// ```rust +/// use permissions_core::{Permission, PermissionBuilder, LocationPrecision}; +/// use permissions_macro::static_permission; +/// +/// // Fine location +/// const LOCATION_FINE: Permission = static_permission!( +/// PermissionBuilder::location(LocationPrecision::Fine) +/// .with_description("Track your runs") +/// .build() +/// ); +/// +/// // Coarse location +/// const LOCATION_COARSE: Permission = static_permission!( +/// PermissionBuilder::location(LocationPrecision::Coarse) +/// .with_description("Approximate location") +/// .build() +/// ); +/// +/// // Custom permission +/// const CUSTOM: Permission = static_permission!( +/// PermissionBuilder::custom() +/// .with_android("android.permission.MY_PERMISSION") +/// .with_ios("NSMyUsageDescription") +/// .with_macos("NSMyUsageDescription") +/// .with_description("Custom permission") +/// .build() +/// ); +/// ``` +/// +/// ## Direct Construction (for simple permissions) +/// +/// Simple permissions like Camera, Microphone, and Notifications use direct construction: +/// ```rust +/// use permissions_core::{Permission, PermissionKind}; +/// use permissions_macro::static_permission; +/// +/// const CAMERA: Permission = static_permission!( +/// Permission::new(PermissionKind::Camera, "Take photos") +/// ); +/// +/// const MICROPHONE: Permission = static_permission!( +/// Permission::new(PermissionKind::Microphone, "Record audio") +/// ); +/// +/// const NOTIFICATIONS: Permission = static_permission!( +/// Permission::new(PermissionKind::Notifications, "Send notifications") +/// ); +/// ``` +/// +/// # Supported Permission Kinds +/// +/// Only tested and verified permissions are included. For any other permissions, +/// use the `Custom` variant with platform-specific identifiers. +/// +/// ## βœ… Tested Permissions (Only for requesting permissions) +/// +/// - `Camera` - Camera access (tested across all platforms) +/// - `Location(Fine)` / `Location(Coarse)` - Location access with precision (tested across all platforms) +/// - `Microphone` - Microphone access (tested across all platforms) +/// - `Notifications` - Push notifications (tested on Android and Web) +/// - `Custom` - Custom permission with platform-specific identifiers +/// +/// See the main documentation for examples of using `Custom` permissions +/// for untested or special use cases. +#[proc_macro] +pub fn static_permission(input: TokenStream) -> TokenStream { + let permission = parse_macro_input!(input as PermissionParser); + + quote! { #permission }.into() +} + +/// Backward compatible alias for [`static_permission!`]. +#[proc_macro] +pub fn permission(input: TokenStream) -> TokenStream { + static_permission(input) +} diff --git a/packages/permissions/permissions-macro/src/linker.rs b/packages/permissions/permissions-macro/src/linker.rs new file mode 100644 index 0000000000..f4b88c8f3a --- /dev/null +++ b/packages/permissions/permissions-macro/src/linker.rs @@ -0,0 +1,19 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; + +/// Generate a linker section for embedding permission data in the binary +/// +/// This function creates a static array containing the serialized permission data +/// wrapped in SymbolData::Permission and exports it with the __ASSETS__ prefix +/// for unified symbol collection with assets. +pub fn generate_link_section(permission: impl ToTokens, permission_hash: &str) -> TokenStream2 { + dx_macro_helpers::linker::generate_link_section( + permission, + permission_hash, + "__ASSETS__", + quote! { permissions::macro_helpers::serialize_permission }, + quote! { permissions::macro_helpers::copy_bytes }, + quote! { permissions::macro_helpers::ConstVec }, + true, // permissions needs #[used] attribute + ) +} diff --git a/packages/permissions/permissions-macro/src/permission.rs b/packages/permissions/permissions-macro/src/permission.rs new file mode 100644 index 0000000000..7cf02602e0 --- /dev/null +++ b/packages/permissions/permissions-macro/src/permission.rs @@ -0,0 +1,51 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use syn::parse::Parse; + +/// Parser for the `static_permission!()` macro syntax (and `permission!()` alias) +/// +/// This parser accepts any expression that evaluates to a `Permission`: +/// - Builder pattern: `PermissionBuilder::location(...).with_description(...).build()` +/// - Direct construction: `Permission::new(PermissionKind::Camera, "...")` +pub struct PermissionParser { + /// The permission expression (either builder or direct) + expr: TokenStream2, +} + +impl Parse for PermissionParser { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // Parse the entire expression as a token stream + // This accepts either: + // - PermissionBuilder::location(...).with_description(...).build() + // - Permission::new(PermissionKind::Camera, "...") + let expr = input.parse::()?; + Ok(Self { expr }) + } +} + +impl ToTokens for PermissionParser { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + // Generate a hash for unique symbol naming + // Hash the expression tokens to create a unique identifier + let mut hash = DefaultHasher::new(); + self.expr.to_string().hash(&mut hash); + let permission_hash = format!("{:016x}", hash.finish()); + + let expr = &self.expr; + let link_section = + crate::linker::generate_link_section(quote!(__PERMISSION), &permission_hash); + + tokens.extend(quote! { + { + // Create the permission instance from the expression + const __PERMISSION: permissions::Permission = #expr; + + #link_section + + // Return the permission + __PERMISSION + } + }); + } +} diff --git a/packages/permissions/permissions/Cargo.toml b/packages/permissions/permissions/Cargo.toml new file mode 100644 index 0000000000..c56b7622fa --- /dev/null +++ b/packages/permissions/permissions/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "permissions" +version = { workspace = true } +edition = "2021" +description = "Cross-platform permission management system with linker-based collection" +authors = ["DioxusLabs"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/permissions" +keywords = ["permissions", "mobile", "desktop", "web", "cross-platform"] +categories = ["development-tools::build-utils"] + +[dependencies] +permissions-core = { workspace = true } +permissions-macro = { workspace = true } +const-serialize = { workspace = true } +dx-macro-helpers = { workspace = true } + +[dev-dependencies] diff --git a/packages/permissions/permissions/README.md b/packages/permissions/permissions/README.md new file mode 100644 index 0000000000..96c980b4ba --- /dev/null +++ b/packages/permissions/permissions/README.md @@ -0,0 +1,146 @@ +# Permissions + +A cross-platform permission management system with linker-based collection, inspired by Manganis. + +This crate provides a unified API for declaring permissions across supported platforms (Android, iOS, macOS) and embeds them in the binary for extraction by build tools. + +## Features + +- **Cross-platform**: Unified API for all platforms +- **Linker-based collection**: Permissions are embedded in the binary using linker sections +- **Type-safe**: Strongly typed permission kinds, not strings +- **Const-time**: All permission data computed at compile time +- **Extensible**: Support for custom permissions with platform-specific identifiers + +## Usage + +### Basic Permission Declaration + +```rust +use permissions::{static_permission, Permission, PermissionBuilder, PermissionKind}; + +// Declare a camera permission using the builder +const CAMERA: Permission = static_permission!( + Permission::new(PermissionKind::Camera, "Take photos") +); + +// Declare a fine-grained location permission +const LOCATION: Permission = static_permission!( + PermissionBuilder::location(permissions::LocationPrecision::Fine) + .with_description("Track your runs") + .build() +); + +// Declare a microphone permission +const MICROPHONE: Permission = static_permission!( + Permission::new(PermissionKind::Microphone, "Record audio") +); +``` + +### Custom Permissions (For Untested or Special Use Cases) + +For permissions that aren't yet tested or for special use cases, use the `Custom` variant +with platform-specific identifiers: + +```rust +use permissions::{static_permission, Permission, PermissionBuilder}; + +// Example: Request storage permission +const STORAGE: Permission = static_permission!( + PermissionBuilder::custom() + .with_android("android.permission.READ_EXTERNAL_STORAGE") + .with_ios("NSPhotoLibraryUsageDescription") + .with_macos("NSPhotoLibraryUsageDescription") + .with_description("Access files on your device") + .build() +); +``` + +> **πŸ’‘ Contributing Back**: If you test a custom permission and verify it works across platforms, +> please consider creating a PR to add it as an officially tested permission! This helps the +> entire Dioxus community. + +### Using Permissions + +```rust +use permissions::{ + static_permission, Permission, PermissionBuilder, PermissionKind, Platform, +}; + +const CAMERA: Permission = static_permission!( + Permission::new(PermissionKind::Camera, "Take photos") +); + +// Get the description +println!("Description: {}", CAMERA.description()); + +// Check platform support +if CAMERA.supports_platform(Platform::Android) { + println!("Android permission: {:?}", CAMERA.android_permission()); +} + +if CAMERA.supports_platform(Platform::Ios) { + println!("iOS key: {:?}", CAMERA.ios_key()); +} + +// Get all platform identifiers +let identifiers = CAMERA.platform_identifiers(); +println!("Android: {:?}", identifiers.android); +println!("iOS: {:?}", identifiers.ios); +println!("macOS: {:?}", identifiers.macos); +``` + +## Supported Permission Kinds + +Only tested and verified permissions are included. For all other permissions, +use the `Custom` variant. + +### βœ… Available Permissions + +- **`Camera`** - Camera access (tested on Android, iOS, macOS) +- **`Location(Fine)` / `Location(Coarse)`** - Location access with precision (tested on Android, iOS, macOS) +- **`Microphone`** - Microphone access (tested on Android, iOS, macOS) +- **`Notifications`** - Push notifications (tested on Android, iOS, macOS) +- **`Custom { ... }`** - Custom permission with platform-specific identifiers + +For examples of untested permissions (like `PhotoLibrary`, `Contacts`, `Calendar`, `Bluetooth`, etc.), +see the Custom Permissions section below. + +## Platform Mappings + +Each permission kind automatically maps to the appropriate platform-specific requirements: + +| Permission | Android | iOS | macOS | +|------------|---------|-----|-------| +| Camera | `android.permission.CAMERA` | `NSCameraUsageDescription` | `NSCameraUsageDescription` | +| Location(Fine) | `android.permission.ACCESS_FINE_LOCATION` | `NSLocationAlwaysAndWhenInUseUsageDescription` | `NSLocationUsageDescription` | +| Microphone | `android.permission.RECORD_AUDIO` | `NSMicrophoneUsageDescription` | `NSMicrophoneUsageDescription` | + +## How It Works + +1. **Declaration**: Use the `static_permission!()` macro (or legacy `permission!()`) to declare permissions in your code +2. **Embedding**: The macro embeds permission data in linker sections with `__PERMISSION__*` symbols +3. **Collection**: Build tools can extract permissions by scanning the binary for these symbols +4. **Injection**: Permissions can be injected into platform-specific configuration files + +## Build Tool Integration + +The embedded `__PERMISSION__*` symbols can be extracted by build tools to: + +- Inject permissions into AndroidManifest.xml +- Inject permissions into iOS Info.plist +- Generate permission request code +- Validate permission usage + +## Examples + +See the `examples/` directory for complete examples of using permissions in different contexts. + +## License + +This project is licensed under either of + +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. diff --git a/packages/permissions/permissions/src/lib.rs b/packages/permissions/permissions/src/lib.rs new file mode 100644 index 0000000000..934f50323f --- /dev/null +++ b/packages/permissions/permissions/src/lib.rs @@ -0,0 +1,77 @@ +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +//! # Permissions +//! +//! A cross-platform permission management system with linker-based collection. +//! +//! This crate provides a unified API for declaring permissions across supported platforms +//! (Android, iOS, macOS) and embeds them in the binary for extraction by build tools. +//! +//! ## Usage +//! +//! ```rust +//! use permissions::{static_permission, Permission}; +//! +//! // Declare a camera permission (static / compile-time) +//! const CAMERA: Permission = static_permission!(Camera, description = "Take photos"); +//! +//! // Declare a location permission with precision +//! const LOCATION: Permission = static_permission!(Location(Fine), description = "Track your runs"); +//! +//! // Use the permission +//! println!("Camera permission: {}", CAMERA.description()); +//! if let Some(android_perm) = CAMERA.android_permission() { +//! println!("Android permission: {}", android_perm); +//! } +//! ``` +//! +//! > **Note:** `permission!` remains available as an alias for `static_permission!` +//! > to preserve backward compatibility with existing code. + +pub use permissions_core::{ + AndroidArtifactMetadata, CustomPermissionBuilder, LocationPrecision, Permission, + PermissionBuilder, PermissionKind, PermissionManifest, Platform, PlatformFlags, + PlatformIdentifiers, SwiftPackageMetadata, SymbolData, +}; +pub use permissions_macro::{permission, static_permission}; + +#[doc(hidden)] +pub mod macro_helpers { + //! Helper functions for macro expansion + //! + //! These functions are used internally by the `static_permission!()` macro (and its `permission!()` alias) + //! and should not be used directly. + + // Re-export const_serialize types for convenience + pub use const_serialize::{self, ConstStr, ConstVec, SerializeConst}; + // Re-export copy_bytes so generated code can use it without dx-macro-helpers dependency + pub use dx_macro_helpers::copy_bytes; + use permissions_core::{AndroidArtifactMetadata, SwiftPackageMetadata}; + pub use permissions_core::{Permission, SymbolData}; + + const fn serialize_symbol_data(symbol_data: SymbolData) -> ConstVec { + let serialized = const_serialize::serialize_const(&symbol_data, ConstVec::new()); + let mut data: ConstVec = ConstVec::new_with_max_size(); + data = data.extend(serialized.as_ref()); + while data.len() < 4096 { + data = data.push(0); + } + data + } + + /// Serialize a permission into a const buffer (wrapped in `SymbolData::Permission`). + pub const fn serialize_permission(permission: &Permission) -> ConstVec { + serialize_symbol_data(SymbolData::Permission(*permission)) + } + + /// Serialize Android artifact metadata (wrapped in `SymbolData::AndroidArtifact`). + pub const fn serialize_android_artifact(meta: &AndroidArtifactMetadata) -> ConstVec { + serialize_symbol_data(SymbolData::AndroidArtifact(*meta)) + } + + /// Serialize Swift package metadata (wrapped in `SymbolData::SwiftPackage`). + pub const fn serialize_swift_package(meta: &SwiftPackageMetadata) -> ConstVec { + serialize_symbol_data(SymbolData::SwiftPackage(*meta)) + } +} diff --git a/packages/permissions/permissions/src/macro_helpers.rs b/packages/permissions/permissions/src/macro_helpers.rs new file mode 100644 index 0000000000..36648af830 --- /dev/null +++ b/packages/permissions/permissions/src/macro_helpers.rs @@ -0,0 +1,2 @@ +// This file is intentionally empty - the macro_helpers module is defined in lib.rs +// to keep the API simple and avoid exposing internal implementation details. diff --git a/packages/platform-bridge-macro/Cargo.toml b/packages/platform-bridge-macro/Cargo.toml new file mode 100644 index 0000000000..0f46b08f2b --- /dev/null +++ b/packages/platform-bridge-macro/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "platform-bridge-macro" +version = { workspace = true } +edition = "2021" +license = "MIT OR Apache-2.0" +description = "Procedural macro for declaring platform plugins with linker embedding" +authors = ["DioxusLabs"] +repository = "https://github.com/DioxusLabs/dioxus" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/platform-bridge-macro" +keywords = ["platform", "bridge", "macro", "linker", "android", "ios"] +categories = ["development-tools::procedural-macro-helpers"] + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" +dx-macro-helpers = { workspace = true } diff --git a/packages/platform-bridge-macro/README.md b/packages/platform-bridge-macro/README.md new file mode 100644 index 0000000000..96070f8d6a --- /dev/null +++ b/packages/platform-bridge-macro/README.md @@ -0,0 +1,76 @@ +# platform-bridge-macro + +Procedural macros for declaring Android and iOS/macOS plugin metadata that the Dioxus CLI collects +from linker symbols. + +## Overview + +The crate exposes two macros: + +- `android_plugin!` β€” declare a prebuilt Android AAR and optional Gradle dependency strings. +- `ios_plugin!` β€” declare a Swift Package (path + product) that was linked into the binary. + +Each macro serializes its metadata as `SymbolData` and emits it under the `__ASSETS__*` linker +prefix, alongside regular assets and permissions. The CLI already performs a single scan of that +prefix after building the Rust binary, so plugin metadata piggy-backs on the same pipeline. + +## Android: `android_plugin!` + +```rust +use dioxus_platform_bridge::android_plugin; + +#[cfg(all(feature = "metadata", target_os = "android"))] +dioxus_platform_bridge::android_plugin!( + plugin = "geolocation", + aar = { env = "DIOXUS_ANDROID_ARTIFACT" }, + deps = ["implementation(\"com.google.android.gms:play-services-location:21.3.0\")"] +); +``` + +### Parameters + +| Name | Required | Description | +|--------|----------|-------------| +| `plugin` | βœ… | Logical plugin identifier used for grouping in diagnostics. | +| `aar` | βœ… | `{ path = "relative/path.aar" }` or `{ env = "ENV_WITH_PATH" }` to locate the artifact. Paths are resolved relative to `CARGO_MANIFEST_DIR`. | +| `deps` | optional | Array of strings (typically Gradle `implementation(...)` lines) appended verbatim to the generated `build.gradle.kts`. | + +The macro resolves the artifact path at compile time, wraps it together with the plugin identifier +and dependency strings in `SymbolData::AndroidArtifact`, and emits it via a linker symbol. No Java +source copying or runtime reflection is involved. + +**CLI behaviour:** while bundling (`dx bundle --android`), the CLI collects every +`SymbolData::AndroidArtifact`, copies the referenced `.aar` into `app/libs/`, and makes sure the +Gradle module depends on it plus any extra `deps` strings. + +## iOS/macOS: `ios_plugin!` + +```rust +use dioxus_platform_bridge::ios_plugin; + +#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))] +dioxus_platform_bridge::ios_plugin!( + plugin = "geolocation", + spm = { path = "ios", product = "GeolocationPlugin" } +); +``` + +### Parameters + +| Name | Required | Description | +|--------|----------|-------------| +| `plugin` | βœ… | Logical plugin identifier. | +| `spm.path` | βœ… | Relative path (from `CARGO_MANIFEST_DIR`) to the Swift package folder. | +| `spm.product` | βœ… | The SwiftPM product name that was linked into the Rust binary. | + +The macro emits `SymbolData::SwiftPackage` entries containing the absolute package path and product +name. The CLI uses those entries as a signal to run `swift-stdlib-tool` and embed the Swift runtime +frameworks when bundling for Apple platforms. + +## Implementation Notes + +- Serialization uses `dx-macro-helpers` which ensures each record is padded to 4β€―KB for consistent + linker output. +- Because all metadata ends up in the shared `__ASSETS__*` section, plugin authors do not need any + additional build stepsβ€”the CLI automatically consumes the data in the same pass as assets and + permissions. diff --git a/packages/platform-bridge-macro/src/android_plugin.rs b/packages/platform-bridge-macro/src/android_plugin.rs new file mode 100644 index 0000000000..823dbc8c06 --- /dev/null +++ b/packages/platform-bridge-macro/src/android_plugin.rs @@ -0,0 +1,162 @@ +use dx_macro_helpers::linker; +use quote::{quote, ToTokens}; +use std::{ + collections::BTreeSet, + hash::{DefaultHasher, Hash, Hasher}, +}; +use syn::{parse::Parse, parse::ParseStream, Token}; + +pub struct AndroidPluginParser { + plugin_name: String, + artifact: ArtifactDeclaration, + dependencies: BTreeSet, +} + +enum ArtifactDeclaration { + Path(String), + Env(String), +} + +impl Parse for AndroidPluginParser { + fn parse(input: ParseStream) -> syn::Result { + let mut plugin_name = None; + let mut artifact = None; + let mut dependencies: BTreeSet = BTreeSet::new(); + + while !input.is_empty() { + let field = input.parse::()?; + match field.to_string().as_str() { + "plugin" => { + let _equals = input.parse::()?; + let plugin_lit = input.parse::()?; + plugin_name = Some(plugin_lit.value()); + let _ = input.parse::>()?; + } + "deps" => { + let _equals = input.parse::()?; + let content; + syn::bracketed!(content in input); + + while !content.is_empty() { + let value = content.parse::()?; + dependencies.insert(value.value()); + let _ = content.parse::>()?; + } + + let _ = input.parse::>()?; + } + "aar" => { + let _equals = input.parse::()?; + let content; + syn::braced!(content in input); + + let mut path = None; + let mut env = None; + + while !content.is_empty() { + let key = content.parse::()?; + let key_str = key.to_string(); + let _eq = content.parse::()?; + let value = content.parse::()?; + match key_str.as_str() { + "path" => path = Some(value.value()), + "env" => env = Some(value.value()), + _ => { + return Err(syn::Error::new( + key.span(), + "Unknown field in aar declaration (expected 'path' or 'env')", + )) + } + } + let _ = content.parse::>()?; + } + + artifact = Some(match (path, env) { + (Some(p), None) => ArtifactDeclaration::Path(p), + (None, Some(e)) => ArtifactDeclaration::Env(e), + (Some(_), Some(_)) => { + return Err(syn::Error::new( + field.span(), + "Specify only one of 'path' or 'env' in aar block", + )) + } + (None, None) => { + return Err(syn::Error::new( + field.span(), + "Missing 'path' or 'env' in aar block", + )) + } + }); + + let _ = input.parse::>()?; + } + _ => { + return Err(syn::Error::new( + field.span(), + "Unknown field, expected 'plugin' or 'aar'", + )); + } + } + } + + Ok(Self { + plugin_name: plugin_name + .ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'plugin'"))?, + artifact: artifact + .ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'aar'"))?, + dependencies, + }) + } +} + +impl ToTokens for AndroidPluginParser { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let plugin_name = &self.plugin_name; + + let mut hash = DefaultHasher::new(); + self.plugin_name.hash(&mut hash); + match &self.artifact { + ArtifactDeclaration::Path(path) => path.hash(&mut hash), + ArtifactDeclaration::Env(env) => env.hash(&mut hash), + } + let plugin_hash = format!("{:016x}", hash.finish()); + + let artifact_expr = match &self.artifact { + ArtifactDeclaration::Path(path) => { + let path_lit = syn::LitStr::new(path, proc_macro2::Span::call_site()); + quote! { concat!(env!("CARGO_MANIFEST_DIR"), "/", #path_lit) } + } + ArtifactDeclaration::Env(var) => { + let env_lit = syn::LitStr::new(var, proc_macro2::Span::call_site()); + quote! { env!(#env_lit) } + } + }; + let deps_joined = self + .dependencies + .iter() + .map(|s| s.as_str()) + .collect::>() + .join("\n"); + let deps_lit = syn::LitStr::new(&deps_joined, proc_macro2::Span::call_site()); + + let metadata_expr = quote! { + dioxus_platform_bridge::android::AndroidArtifactMetadata::new( + #plugin_name, + #artifact_expr, + #deps_lit, + ) + }; + + let link_section = linker::generate_link_section( + metadata_expr, + &plugin_hash, + "__ASSETS__", + quote! { dioxus_platform_bridge::android::metadata::serialize_android_metadata }, + quote! { dioxus_platform_bridge::android::macro_helpers::copy_bytes }, + quote! { dioxus_platform_bridge::android::metadata::AndroidMetadataBuffer }, + true, + ); + + tokens.extend(link_section); + } +} diff --git a/packages/platform-bridge-macro/src/ios_plugin.rs b/packages/platform-bridge-macro/src/ios_plugin.rs new file mode 100644 index 0000000000..0e7ca6f87a --- /dev/null +++ b/packages/platform-bridge-macro/src/ios_plugin.rs @@ -0,0 +1,121 @@ +use dx_macro_helpers::linker; +use quote::{quote, ToTokens}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use syn::{parse::Parse, parse::ParseStream, Token}; + +/// Parser for the `ios_plugin!()` macro syntax +pub struct IosPluginParser { + /// Plugin identifier (e.g., "geolocation") + plugin_name: String, + /// Swift Package declaration + spm: SpmDeclaration, +} + +#[derive(Clone)] +struct SpmDeclaration { + path: String, + product: String, +} + +impl Parse for IosPluginParser { + fn parse(input: ParseStream) -> syn::Result { + let mut plugin_name = None; + let mut spm = None; + + while !input.is_empty() { + let field = input.parse::()?; + match field.to_string().as_str() { + "plugin" => { + let _equals = input.parse::()?; + let plugin_lit = input.parse::()?; + plugin_name = Some(plugin_lit.value()); + let _ = input.parse::>()?; + } + "spm" => { + let _equals = input.parse::()?; + let content; + syn::braced!(content in input); + + let mut path = None; + let mut product = None; + while !content.is_empty() { + let key = content.parse::()?; + let key_str = key.to_string(); + let _eq = content.parse::()?; + let value = content.parse::()?; + match key_str.as_str() { + "path" => path = Some(value.value()), + "product" => product = Some(value.value()), + _ => return Err(syn::Error::new( + key.span(), + "Unknown field in spm declaration (expected 'path' or 'product')", + )), + } + let _ = content.parse::>()?; + } + + let path = path.ok_or_else(|| { + syn::Error::new(field.span(), "Missing required field 'path' in spm block") + })?; + let product = product.ok_or_else(|| { + syn::Error::new( + field.span(), + "Missing required field 'product' in spm block", + ) + })?; + spm = Some(SpmDeclaration { path, product }); + + let _ = input.parse::>()?; + } + _ => { + return Err(syn::Error::new( + field.span(), + "Unknown field, expected 'plugin' or 'spm'", + )); + } + } + } + + Ok(Self { + plugin_name: plugin_name + .ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'plugin'"))?, + spm: spm + .ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'spm'"))?, + }) + } +} + +impl ToTokens for IosPluginParser { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let plugin_name = &self.plugin_name; + + let mut hash = DefaultHasher::new(); + self.plugin_name.hash(&mut hash); + self.spm.path.hash(&mut hash); + self.spm.product.hash(&mut hash); + let plugin_hash = format!("{:016x}", hash.finish()); + + let path_lit = syn::LitStr::new(&self.spm.path, proc_macro2::Span::call_site()); + let product_lit = syn::LitStr::new(&self.spm.product, proc_macro2::Span::call_site()); + + let metadata_expr = quote! { + dioxus_platform_bridge::darwin::SwiftSourceMetadata::new( + #plugin_name, + concat!(env!("CARGO_MANIFEST_DIR"), "/", #path_lit), + #product_lit, + ) + }; + + let link_section = linker::generate_link_section( + metadata_expr, + &plugin_hash, + "__ASSETS__", + quote! { dioxus_platform_bridge::darwin::metadata::serialize_swift_metadata }, + quote! { dioxus_platform_bridge::android::macro_helpers::copy_bytes }, + quote! { dioxus_platform_bridge::darwin::metadata::SwiftMetadataBuffer }, + true, + ); + + tokens.extend(link_section); + } +} diff --git a/packages/platform-bridge-macro/src/lib.rs b/packages/platform-bridge-macro/src/lib.rs new file mode 100644 index 0000000000..9fb430203b --- /dev/null +++ b/packages/platform-bridge-macro/src/lib.rs @@ -0,0 +1,101 @@ +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +mod android_plugin; +mod ios_plugin; + +/// Declare an Android plugin that will be embedded in the binary +/// +/// This macro declares prebuilt Android artifacts (AARs) and embeds their metadata into the compiled +/// binary using the shared `SymbolData` stream (the same linker section used for assets and +/// permissions). The Dioxus CLI reads that metadata to copy the AARs into the generated Gradle +/// project and to append any additional Gradle dependencies. +/// +/// # Syntax +/// +/// Basic plugin declaration with full relative paths: +/// ```rust,no_run +/// #[cfg(target_os = "android")] +/// dioxus_platform_bridge::android_plugin!( +/// plugin = "geolocation", +/// aar = { path = "android/build/outputs/aar/geolocation-plugin-release.aar" } +/// ); +/// ``` +/// +/// # Parameters +/// +/// - `plugin`: The plugin identifier for organization (e.g., "geolocation") +/// - `aar`: A block with either `{ path = "relative/path/to.aar" }` or `{ env = "ENV_WITH_PATH" }` +/// +/// When `path` is used, it is resolved relative to `CARGO_MANIFEST_DIR`. When `env` is used, +/// the environment variable is read at compile time via `env!`. +/// +/// The macro wraps the resolved artifact path and dependency strings in +/// `SymbolData::AndroidArtifact` and stores it under the `__ASSETS__*` linker prefix. Because the CLI +/// already scans that prefix for assets and permissions, no extra scanner is required. +/// +/// # Example Structure +/// +/// ```text +/// your-plugin-crate/ +/// └── android/ +/// β”œβ”€β”€ build.gradle.kts # Builds the AAR +/// β”œβ”€β”€ settings.gradle.kts +/// └── build/outputs/aar/ +/// └── geolocation-plugin-release.aar +/// ``` +#[proc_macro] +pub fn android_plugin(input: TokenStream) -> TokenStream { + let android_plugin = parse_macro_input!(input as android_plugin::AndroidPluginParser); + + quote! { #android_plugin }.into() +} + +/// Declare an iOS/macOS plugin that will be embedded in the binary +/// +/// This macro declares Swift packages and embeds their metadata into the compiled binary using the +/// shared `SymbolData` stream. The Dioxus CLI uses this metadata to ensure the Swift runtime is +/// bundled correctly whenever Swift code is linked. +/// +/// # Syntax +/// +/// Basic plugin declaration: +/// ```rust,no_run +/// #[cfg(any(target_os = "ios", target_os = "macos"))] +/// dioxus_platform_bridge::ios_plugin!( +/// plugin = "geolocation", +/// spm = { path = "ios", product = "GeolocationPlugin" } +/// ); +/// ``` +/// +/// # Parameters +/// +/// - `plugin`: The plugin identifier for organization (e.g., "geolocation") +/// - `spm`: A Swift Package declaration with `{ path = "...", product = "MyPlugin" }` relative to +/// `CARGO_MANIFEST_DIR`. +/// +/// The macro expands paths using `env!("CARGO_MANIFEST_DIR")` so package manifests are +/// resolved relative to the crate declaring the plugin. +/// +/// The metadata is serialized as `SymbolData::SwiftPackage` and emitted under the `__ASSETS__*` +/// prefix, alongside assets, permissions, and Android artifacts. +/// +/// # Example Structure +/// +/// ```text +/// your-plugin-crate/ +/// └── ios/ +/// β”œβ”€β”€ Package.swift +/// └── Sources/ +/// └── GeolocationPlugin.swift +/// ``` +#[proc_macro] +pub fn ios_plugin(input: TokenStream) -> TokenStream { + let ios_plugin = parse_macro_input!(input as ios_plugin::IosPluginParser); + + quote! { #ios_plugin }.into() +} diff --git a/packages/platform-bridge/Cargo.toml b/packages/platform-bridge/Cargo.toml new file mode 100644 index 0000000000..ebeff2fed4 --- /dev/null +++ b/packages/platform-bridge/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "dioxus-platform-bridge" +version = { workspace = true } +edition = "2021" +license = "MIT OR Apache-2.0" +description = "FFI utilities and plugin metadata for Dioxus mobile platform APIs" +repository = "https://github.com/DioxusLabs/dioxus" +keywords = ["dioxus", "platform", "bridge", "ffi", "android", "ios", "macos", "jni", "objc"] +categories = ["gui", "platform-support"] + +[features] +default = [] +metadata = [ + "dep:const-serialize", + "dep:const-serialize-macro", + "dep:platform-bridge-macro", + "dep:permissions", +] + +[dependencies] +thiserror = { workspace = true } +const-serialize = { workspace = true, optional = true } +const-serialize-macro = { workspace = true, optional = true } +platform-bridge-macro = { workspace = true, optional = true } +permissions = { workspace = true, optional = true } + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.21" +ndk-context = "0.1.1" + +[target.'cfg(target_os = "ios")'.dependencies] +objc2 = "0.6.3" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6.3" + +[package.metadata.docs.rs] +targets = ["aarch64-linux-android", "aarch64-apple-ios"] diff --git a/packages/platform-bridge/README.md b/packages/platform-bridge/README.md new file mode 100644 index 0000000000..ae9568fc03 --- /dev/null +++ b/packages/platform-bridge/README.md @@ -0,0 +1,77 @@ +# dioxus-platform-bridge + +FFI utilities and plugin metadata for Dioxus mobile platform APIs. + +This crate provides common patterns and utilities for implementing mobile platform APIs in Dioxus applications. It handles the boilerplate for JNI (Android) and objc2 (iOS) bindings, build scripts, and platform-specific resource management. + +## Features + +- **Android Support**: JNI utilities, activity caching, DEX loading, callback registration +- **iOS/macOS Support**: Main thread utilities, manager caching, objc2 integration +- **Metadata System**: Declarative macros (`android_plugin!`, `ios_plugin!`) for embedding platform artifacts that the Dioxus CLI collects from linker symbols + +## Usage + +### Android APIs + +```rust +use dioxus_platform_bridge::android::with_activity; + +// Execute JNI operations with cached activity reference +let result = with_activity(|env, activity| { + // Your JNI operations here + Some(42) +}); +``` + +### iOS/macOS APIs + +```rust +use dioxus_platform_bridge::darwin::MainThreadCell; +use objc2::MainThreadMarker; + +let mtm = MainThreadMarker::new().unwrap(); +let cell = MainThreadCell::new(); +let value = cell.get_or_init_with(mtm, || "initialized"); +``` + +### Declaring Android Plugins + +Declare Gradle artifacts (AARs) plus extra Gradle dependency lines. The metadata is embedded in the +Rust binary and discovered by the Dioxus CLI when bundling user apps: + +```rust +use dioxus_platform_bridge::android_plugin; + +#[cfg(all(feature = "metadata", target_os = "android"))] +dioxus_platform_bridge::android_plugin!( + plugin = "geolocation", + aar = { env = "DIOXUS_ANDROID_ARTIFACT" }, + deps = ["implementation(\"com.google.android.gms:play-services-location:21.3.0\")"] +); +``` + +### Declaring iOS/macOS Swift Packages + +Declare Swift packages for iOS/macOS builds: + +```rust +use dioxus_platform_bridge::ios_plugin; + +#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))] +dioxus_platform_bridge::ios_plugin!( + plugin = "geolocation", + spm = { path = "ios", product = "GeolocationPlugin" } +); +``` + +## Architecture + +The crate is organized into platform-specific modules: + +- `android/` - JNI utilities, activity management, callback systems, Android metadata helpers +- `darwin/` - Main thread utilities for iOS and macOS (objc2) + +## License + +MIT OR Apache-2.0 diff --git a/packages/platform-bridge/src/android/activity.rs b/packages/platform-bridge/src/android/activity.rs new file mode 100644 index 0000000000..d73b1ef413 --- /dev/null +++ b/packages/platform-bridge/src/android/activity.rs @@ -0,0 +1,63 @@ +use jni::{objects::JObject, JNIEnv, JavaVM}; +use std::sync::OnceLock; + +/// Cached reference to the Android activity. +static ACTIVITY: OnceLock = OnceLock::new(); +static JAVA_VM: OnceLock = OnceLock::new(); + +/// Execute a JNI operation with a cached activity reference. +/// +/// This function handles the boilerplate of getting the JavaVM and Activity +/// references, caching them for subsequent calls. It's the foundation for +/// most Android mobile API operations. +/// +/// # Arguments +/// +/// * `f` - A closure that receives a mutable JNIEnv and the Activity JObject +/// +/// # Returns +/// +/// Returns `Some(R)` if the operation succeeds, `None` if there's an error +/// getting the VM or Activity references. +/// +/// # Example +/// +/// ```rust,no_run +/// use dioxus_platform_bridge::android::with_activity; +/// +/// let result = with_activity(|env, activity| { +/// // Your JNI operations here +/// Some(42) +/// }); +/// ``` +pub fn with_activity(f: F) -> Option +where + F: FnOnce(&mut JNIEnv<'_>, &JObject<'_>) -> Option, +{ + let ctx = ndk_context::android_context(); + let vm = if let Some(vm) = JAVA_VM.get() { + vm + } else { + let raw_vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) }.ok()?; + let _ = JAVA_VM.set(raw_vm); + JAVA_VM.get()? + }; + let mut env = vm.attach_current_thread().ok()?; + + let activity = if let Some(activity) = ACTIVITY.get() { + activity + } else { + let raw_activity = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) }; + let global = env.new_global_ref(&raw_activity).ok()?; + match ACTIVITY.set(global) { + Ok(()) => ACTIVITY.get().unwrap(), + Err(global) => { + drop(global); + ACTIVITY.get()? + } + } + }; + + let activity_obj = activity.as_obj(); + f(&mut env, &activity_obj) +} diff --git a/packages/platform-bridge/src/android/callback.rs b/packages/platform-bridge/src/android/callback.rs new file mode 100644 index 0000000000..de2f3f9c81 --- /dev/null +++ b/packages/platform-bridge/src/android/callback.rs @@ -0,0 +1,136 @@ +use jni::{ + objects::{GlobalRef, JClass, JObject, JValue}, + sys::jlong, + JNIEnv, NativeMethod, +}; + +use crate::android::java::Result; + +/// Generic callback system for loading DEX classes and registering native methods +pub struct CallbackSystem { + bytecode: &'static [u8], + class_name: &'static str, + callback_name: &'static str, + callback_signature: &'static str, +} + +impl CallbackSystem { + /// Create a new callback system + /// + /// # Arguments + /// + /// * `bytecode` - The compiled DEX bytecode + /// * `class_name` - The fully qualified Java class name + /// * `callback_name` - The name of the native callback method + /// * `callback_signature` - The JNI signature of the callback method + pub fn new( + bytecode: &'static [u8], + class_name: &'static str, + callback_name: &'static str, + callback_signature: &'static str, + ) -> Self { + Self { + bytecode, + class_name, + callback_name, + callback_signature, + } + } + + /// Load the DEX class and register the native callback method + /// + /// This function handles the boilerplate of: + /// 1. Creating an InMemoryDexClassLoader + /// 2. Loading the specified class + /// 3. Registering the native callback method + /// + /// # Returns + /// + /// Returns a `GlobalRef` to the loaded class, or an error if loading fails + pub fn load_and_register(&self, env: &mut JNIEnv<'_>) -> Result { + let callback_class = self.load_dex_class(env)?; + self.register_native_callback(env, &callback_class)?; + let global = env.new_global_ref(callback_class)?; + Ok(global) + } + + /// Load the DEX class from bytecode + fn load_dex_class<'a>(&self, env: &mut JNIEnv<'a>) -> Result> { + const IN_MEMORY_LOADER: &str = "dalvik/system/InMemoryDexClassLoader"; + + // Create a ByteBuffer from our DEX bytecode + let byte_buffer = unsafe { + env.new_direct_byte_buffer(self.bytecode.as_ptr() as *mut u8, self.bytecode.len()) + }?; + + // Create an InMemoryDexClassLoader with our DEX bytecode + let dex_class_loader = env.new_object( + IN_MEMORY_LOADER, + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V", + &[ + JValue::Object(&byte_buffer), + JValue::Object(&JObject::null()), + ], + )?; + + // Load our class + let class_name = env.new_string(self.class_name)?; + let class = env + .call_method( + &dex_class_loader, + "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;", + &[JValue::Object(&class_name)], + )? + .l()?; + + Ok(class.into()) + } + + /// Register the native callback method with the Java class + fn register_native_callback<'a>( + &self, + env: &mut JNIEnv<'a>, + callback_class: &JClass<'a>, + ) -> Result<()> { + env.register_native_methods( + callback_class, + &[NativeMethod { + name: self.callback_name.into(), + sig: self.callback_signature.into(), + fn_ptr: rust_callback as *mut _, + }], + )?; + Ok(()) + } +} + +/// Generic native callback function called from Java +/// +/// SAFETY: This function is called from Java and must maintain proper memory safety. +/// The handler pointer is valid as long as the Manager exists (see Drop implementation). +#[no_mangle] +unsafe extern "C" fn rust_callback<'a>( + mut env: JNIEnv<'a>, + _class: JObject<'a>, + handler_ptr_high: jlong, + handler_ptr_low: jlong, + location: JObject<'a>, +) { + // Reconstruct the pointer from two i64 values (for 64-bit pointers) + #[cfg(not(target_pointer_width = "64"))] + compile_error!("Only 64-bit Android targets are supported"); + + let handler_ptr_raw: usize = + ((handler_ptr_high as u64) << 32 | handler_ptr_low as u64) as usize; + + // Convert to our callback function pointer + let callback: fn(&mut JNIEnv, JObject) = unsafe { std::mem::transmute(handler_ptr_raw) }; + + // Create a global reference to the location object + if let Ok(global_location) = env.new_global_ref(&location) { + callback(&mut env, unsafe { + JObject::from_raw(global_location.as_obj().as_raw()) + }); + } +} diff --git a/packages/platform-bridge/src/android/java.rs b/packages/platform-bridge/src/android/java.rs new file mode 100644 index 0000000000..fd44731ee1 --- /dev/null +++ b/packages/platform-bridge/src/android/java.rs @@ -0,0 +1,127 @@ +use jni::{ + objects::{JClass, JObject, JObjectArray, JString, JValue, JValueOwned}, + JNIEnv, +}; + +/// Result type for JNI operations +pub type Result = std::result::Result; + +/// Helper functions for common JNI operations + +/// Create a new Java string tied to the current JNI frame +pub fn new_string<'env>(env: &mut JNIEnv<'env>, s: &str) -> Result> { + env.new_string(s) +} + +/// Create a new object array +pub fn new_object_array<'env>( + env: &mut JNIEnv<'env>, + len: i32, + element_class: &str, +) -> Result> { + env.new_object_array(len, element_class, &JObject::null()) +} + +/// Set an element in an object array +pub fn set_object_array_element<'env>( + env: &mut JNIEnv<'env>, + array: &JObjectArray<'env>, + index: i32, + element: JString<'env>, +) -> Result<()> { + env.set_object_array_element(array, index, element) +} + +/// Call a static method on a class +pub fn call_static_method<'env, 'obj>( + env: &mut JNIEnv<'env>, + class: &JClass<'env>, + method_name: &str, + signature: &str, + args: &[JValue<'env, 'obj>], +) -> Result> { + env.call_static_method(class, method_name, signature, args) +} + +/// Call an instance method on an object +pub fn call_method<'env, 'obj>( + env: &mut JNIEnv<'env>, + obj: &JObject<'env>, + method_name: &str, + signature: &str, + args: &[JValue<'env, 'obj>], +) -> Result> { + env.call_method(obj, method_name, signature, args) +} + +/// Find a Java class by name +pub fn find_class<'env>(env: &mut JNIEnv<'env>, class_name: &str) -> Result> { + env.find_class(class_name) +} + +/// Create a new object instance +pub fn new_object<'env, 'obj>( + env: &mut JNIEnv<'env>, + class_name: &str, + signature: &str, + args: &[JValue<'env, 'obj>], +) -> Result> { + env.new_object(class_name, signature, args) +} + +/// Check if a permission is granted (Activity.checkSelfPermission) +pub fn check_self_permission( + env: &mut JNIEnv, + activity: &JObject, + permission: &str, +) -> Result { + let permission_string = new_string(env, permission)?; + let status = env.call_method( + activity, + "checkSelfPermission", + "(Ljava/lang/String;)I", + &[JValue::Object(&permission_string)], + )?; + + const PERMISSION_GRANTED: i32 = 0; + Ok(status.i()? == PERMISSION_GRANTED) +} + +/// Request permissions via a helper class's static method +/// +/// This uses PermissionsHelper.requestPermissionsOnUiThread(pattern) +/// to request permissions on the UI thread. +pub fn request_permissions_via_helper( + env: &mut JNIEnv, + helper_class: &JClass, + activity: &JObject, + permissions: JObjectArray, + request_code: i32, +) -> Result<()> { + env.call_static_method( + helper_class, + "requestPermissionsOnUiThread", + "(Landroid/app/Activity;[Ljava/lang/String;I)V", + &[ + JValue::Object(activity), + JValue::Object(&permissions.into()), + JValue::Int(request_code), + ], + )?; + Ok(()) +} + +/// Load a Java class from the APK's classloader +pub fn load_class_from_classloader<'env>( + env: &mut JNIEnv<'env>, + class_name: &str, +) -> Result> { + let class_name_jstring = new_string(env, class_name)?; + let class = env.call_static_method( + "java/lang/Class", + "forName", + "(Ljava/lang/String;)Ljava/lang/Class;", + &[JValue::Object(&class_name_jstring)], + )?; + Ok(class.l()?.into()) +} diff --git a/packages/platform-bridge/src/android/metadata.rs b/packages/platform-bridge/src/android/metadata.rs new file mode 100644 index 0000000000..c7d126602b --- /dev/null +++ b/packages/platform-bridge/src/android/metadata.rs @@ -0,0 +1,12 @@ +//! Android metadata wrappers for linker-based collection. + +#[cfg(feature = "metadata")] +pub use permissions::AndroidArtifactMetadata; + +#[cfg(feature = "metadata")] +pub type AndroidMetadataBuffer = permissions::macro_helpers::ConstVec; + +#[cfg(feature = "metadata")] +pub const fn serialize_android_metadata(meta: &AndroidArtifactMetadata) -> AndroidMetadataBuffer { + permissions::macro_helpers::serialize_android_artifact(meta) +} diff --git a/packages/platform-bridge/src/android/mod.rs b/packages/platform-bridge/src/android/mod.rs new file mode 100644 index 0000000000..2f172eae7c --- /dev/null +++ b/packages/platform-bridge/src/android/mod.rs @@ -0,0 +1,39 @@ +//! Android-specific utilities for mobile APIs + +#[cfg(target_os = "android")] +pub mod activity; +#[cfg(target_os = "android")] +pub mod callback; +#[cfg(target_os = "android")] +pub mod java; +#[cfg(any(target_os = "android", feature = "metadata"))] +pub mod metadata; + +#[doc(hidden)] +pub mod macro_helpers { + //! Helper functions for macro expansion + //! + //! These functions are used internally by the `android_plugin!()` macro + //! and should not be used directly. + + /// Copy a slice into a constant sized buffer at compile time + pub const fn copy_bytes(bytes: &[u8]) -> [u8; N] { + let mut out = [0; N]; + let mut i = 0; + while i < N { + out[i] = bytes[i]; + i += 1; + } + out + } +} + +#[cfg(target_os = "android")] +pub use activity::*; +#[cfg(target_os = "android")] +pub use callback::*; +#[cfg(target_os = "android")] +pub use java::*; + +#[cfg(feature = "metadata")] +pub use metadata::AndroidArtifactMetadata; diff --git a/packages/platform-bridge/src/darwin/manager.rs b/packages/platform-bridge/src/darwin/manager.rs new file mode 100644 index 0000000000..137e03b989 --- /dev/null +++ b/packages/platform-bridge/src/darwin/manager.rs @@ -0,0 +1,83 @@ +use objc2::MainThreadMarker; +use std::cell::UnsafeCell; + +/// A cell that stores values only accessible on the main thread. +/// +/// This type is useful for caching singleton-like objects that must only be +/// accessed on the main thread on Darwin platforms (iOS/macOS). +/// +/// # Safety +/// +/// Access is guarded by requiring a `MainThreadMarker`, ensuring this cell +/// is only touched from the main thread. +/// +/// # Example +/// +/// ```rust,no_run +/// use dioxus_platform_bridge::darwin::MainThreadCell; +/// use objc2::MainThreadMarker; +/// +/// let mtm = MainThreadMarker::new().unwrap(); +/// let cell = MainThreadCell::new(); +/// let value = cell.get_or_init_with(mtm, || "initialized"); +/// ``` +pub struct MainThreadCell(UnsafeCell>); + +impl MainThreadCell { + /// Create a new empty cell. + pub const fn new() -> Self { + Self(UnsafeCell::new(None)) + } +} + +impl Default for MainThreadCell { + fn default() -> Self { + Self::new() + } +} + +impl MainThreadCell { + /// Get or initialize the value in this cell. + /// + /// Requires a `MainThreadMarker` to ensure we're on the main thread. + /// The `init` closure is only called if the cell is currently empty. + /// + /// # Panics + /// + /// This will panic if the value has not been initialized after calling + /// the init closure. This should not happen in practice but is a safety + /// check to ensure thread safety. + pub fn get_or_init_with(&self, _mtm: MainThreadMarker, init: F) -> &T + where + F: FnOnce() -> T, + { + // SAFETY: Access is guarded by requiring a `MainThreadMarker`, so this + // is only touched from the main thread. + unsafe { + let slot = &mut *self.0.get(); + if slot.is_none() { + *slot = Some(init()); + } + slot.as_ref().expect("Manager initialized") + } + } + + /// Fallible variant of [`get_or_init_with`] that allows returning an error during initialization. + pub fn get_or_try_init_with(&self, _mtm: MainThreadMarker, init: F) -> Result<&T, E> + where + F: FnOnce() -> Result, + { + unsafe { + let slot = &mut *self.0.get(); + if slot.is_none() { + *slot = Some(init()?); + } + Ok(slot.as_ref().expect("Manager initialized")) + } + } +} + +// SAFETY: `MainThreadCell` enforces main-thread-only access through +// `MainThreadMarker`. Multiple threads can hold references to the same cell, +// but all access must happen on the main thread through the `MainThreadMarker`. +unsafe impl Sync for MainThreadCell {} diff --git a/packages/platform-bridge/src/darwin/metadata.rs b/packages/platform-bridge/src/darwin/metadata.rs new file mode 100644 index 0000000000..04875925ae --- /dev/null +++ b/packages/platform-bridge/src/darwin/metadata.rs @@ -0,0 +1,12 @@ +//! Swift package metadata wrappers for linker-based collection. + +#[cfg(feature = "metadata")] +pub use permissions::SwiftPackageMetadata as SwiftSourceMetadata; + +#[cfg(feature = "metadata")] +pub type SwiftMetadataBuffer = permissions::macro_helpers::ConstVec; + +#[cfg(feature = "metadata")] +pub const fn serialize_swift_metadata(meta: &SwiftSourceMetadata) -> SwiftMetadataBuffer { + permissions::macro_helpers::serialize_swift_package(meta) +} diff --git a/packages/platform-bridge/src/darwin/mod.rs b/packages/platform-bridge/src/darwin/mod.rs new file mode 100644 index 0000000000..39f6a09513 --- /dev/null +++ b/packages/platform-bridge/src/darwin/mod.rs @@ -0,0 +1,18 @@ +//! Darwin (iOS/macOS) shared utilities for objc2-based APIs +//! +//! This module provides shared utilities for both iOS and macOS platforms +//! since they share the same Objective-C runtime and threading requirements +//! through objc2. + +pub mod manager; + +#[cfg(feature = "metadata")] +pub mod metadata; + +pub use manager::*; + +/// Re-export MainThreadMarker for convenience +pub use objc2::MainThreadMarker; + +#[cfg(feature = "metadata")] +pub use metadata::*; diff --git a/packages/platform-bridge/src/lib.rs b/packages/platform-bridge/src/lib.rs new file mode 100644 index 0000000000..b35c6bc538 --- /dev/null +++ b/packages/platform-bridge/src/lib.rs @@ -0,0 +1,33 @@ +//! FFI utilities and plugin metadata for Dioxus mobile platform APIs +//! +//! This crate provides common patterns and utilities for implementing +//! mobile platform APIs in Dioxus applications. It handles the +//! boilerplate for JNI (Android) and objc2 (iOS/macOS) bindings, build scripts, +//! and platform-specific resource management. + +#[cfg(any(target_os = "android", feature = "metadata"))] +pub mod android; + +#[cfg(any(target_os = "ios", target_os = "macos"))] +pub mod darwin; + +#[cfg(target_os = "android")] +pub use android::*; + +#[cfg(any(target_os = "ios", target_os = "macos"))] +pub use darwin::*; + +/// Re-export commonly used types for convenience +#[cfg(target_os = "android")] +pub use jni; + +#[cfg(any(target_os = "ios", target_os = "macos"))] +pub use objc2; + +/// Re-export the android_plugin! macro when metadata feature is enabled +#[cfg(all(feature = "metadata", any(target_os = "android", feature = "metadata")))] +pub use platform_bridge_macro::android_plugin; + +/// Re-export the ios_plugin! macro when metadata feature is enabled +#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))] +pub use platform_bridge_macro::ios_plugin; diff --git a/packages/playwright-tests/cli-optimization.spec.js b/packages/playwright-tests/cli-optimization.spec.js index 28b83d84c7..6f3e18e1d6 100644 --- a/packages/playwright-tests/cli-optimization.spec.js +++ b/packages/playwright-tests/cli-optimization.spec.js @@ -1,59 +1,67 @@ // @ts-check const { test, expect } = require("@playwright/test"); -test("optimized scripts run", async ({ page }) => { - await page.goto("http://localhost:8989"); - - // // Expect the page to load the script after optimizations have been applied. The script - // // should add an editor to the page that shows a main function - // const main = page.locator("#main"); - // await expect(main).toContainText("hi"); - - // Expect the page to contain an image with the id "some_image" - const image = page.locator("#some_image"); - await expect(image).toBeVisible(); - // Get the image src - const src = await image.getAttribute("src"); - - // Expect the page to contain an image with the id "some_image_with_the_same_url" - const image2 = page.locator("#some_image_with_the_same_url"); - await expect(image2).toBeVisible(); - // Get the image src - const src2 = await image2.getAttribute("src"); - - // Expect the urls to be different - expect(src).not.toEqual(src2); - - // Expect the page to contain an image with the id "some_image_without_hash" - const image3 = page.locator("#some_image_without_hash"); - await expect(image3).toBeVisible(); - // Get the image src - const src3 = await image3.getAttribute("src"); - // Expect the src to be without a hash - expect(src3).toEqual("/assets/toasts.avif"); -}); - -test("unused external assets are bundled", async ({ page }) => { - await page.goto("http://localhost:8989"); - - // Assert http://localhost:8989/assets/toasts.png is found even though it is not used in the page - const response = await page.request.get( - "http://localhost:8989/assets/toasts.png" - ); - // Expect the response to be ok - expect(response.status()).toBe(200); - // make sure the response is an image - expect(response.headers()["content-type"]).toBe("image/png"); -}); - -test("assets are resolved", async ({ page }) => { - await page.goto("http://localhost:8989"); - - // Expect the page to contain an element with the id "resolved-data" - const resolvedData = page.locator("#resolved-data"); - await expect(resolvedData).toBeVisible(); - // Expect the element to contain the text "List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" - await expect(resolvedData).toContainText( - "List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" - ); -}); +const test_variants = [ + { port: 9090, name: "0.7.1" }, + { port: 8989, name: "current version" }, +]; + +for (let { port, name } of test_variants) { + test(`optimized scripts run in ${name}`, async ({ page }) => { + await page.goto(`http://localhost:${port}`); + + // // Expect the page to load the script after optimizations have been applied. The script + // // should add an editor to the page that shows a main function + // const main = page.locator("#main"); + // await expect(main).toContainText("hi"); + + // Expect the page to contain an image with the id "some_image" + const image = page.locator("#some_image"); + await expect(image).toBeVisible(); + // Get the image src + const src = await image.getAttribute("src"); + + // Expect the page to contain an image with the id "some_image_with_the_same_url" + const image2 = page.locator("#some_image_with_the_same_url"); + await expect(image2).toBeVisible(); + // Get the image src + const src2 = await image2.getAttribute("src"); + + // Expect the urls to be different + expect(src).not.toEqual(src2); + + // Expect the page to contain an image with the id "some_image_without_hash" + const image3 = page.locator("#some_image_without_hash"); + await expect(image3).toBeVisible(); + // Get the image src + const src3 = await image3.getAttribute("src"); + // Expect the src to be without a hash + expect(src3).toEqual("/assets/toasts.avif"); + }); + + test(`unused external assets are bundled in ${name}`, async ({ page }) => { + await page.goto(`http://localhost:${port}`); + + // Assert http://localhost:9090/assets/toasts.png is found even though it is not used in the page + const response = await page.request.get( + "http://localhost:9090/assets/toasts.png" + ); + // Expect the response to be ok + expect(response.status()).toBe(200); + // make sure the response is an image + expect(response.headers()["content-type"]).toBe("image/png"); + }); + + test(`assets are resolved in ${name}`, async ({ page }) => { + await page.goto(`http://localhost:${port}`); + + // Expect the page to contain an element with the id "resolved-data" + const resolvedData = page.locator("#resolved-data"); + await expect(resolvedData).toBeVisible(); + // Expect the element to contain the text "List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + await expect(resolvedData).toContainText( + "List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + ); + }); + +} diff --git a/packages/playwright-tests/cli-optimization/Cargo.toml b/packages/playwright-tests/cli-optimization/Cargo.toml index 29d7ae2e47..e7519c8abb 100644 --- a/packages/playwright-tests/cli-optimization/Cargo.toml +++ b/packages/playwright-tests/cli-optimization/Cargo.toml @@ -7,7 +7,9 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -dioxus = { workspace = true, features = ["web"] } +dioxus = { workspace = true, features = ["web"], optional = true } +# We test both if the current version of dioxus works and if the CLI can understand assets from the old asset format +dioxus_07 = { package = "dioxus", version = "=0.7.1", features = ["web"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true @@ -15,3 +17,8 @@ serde_json.workspace = true # reqwest = { workspace = true, features = ["blocking"] } # flate2 = "1.1.2" # tar = "0.4.44" + +[features] +default = ["dioxus"] +dioxus = ["dep:dioxus"] +dioxus_07 = ["dep:dioxus_07"] diff --git a/packages/playwright-tests/cli-optimization/src/main.rs b/packages/playwright-tests/cli-optimization/src/main.rs index cd7f590c35..252c3946cd 100644 --- a/packages/playwright-tests/cli-optimization/src/main.rs +++ b/packages/playwright-tests/cli-optimization/src/main.rs @@ -1,5 +1,8 @@ // This test checks the CLI optimizes assets correctly without breaking them +#[cfg(feature = "dioxus_07")] +use dioxus_07 as dioxus; + use dioxus::prelude::*; const SOME_IMAGE: Asset = asset!("/images/toasts.png", AssetOptions::image().with_avif()); diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index c90c80df3f..7e394ce6ce 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -172,6 +172,16 @@ module.exports = defineConfig({ reuseExistingServer: !process.env.CI, stdout: "pipe", }, + { + cwd: path.join(process.cwd(), "cli-optimization"), + // Remove the cache folder for the cli-optimization build to force a full cache reset + command: + 'cargo run --package dioxus-cli --release -- run --addr "127.0.0.1" --port 9090 --no-default-features --features dioxus_07', + port: 9090, + timeout: 50 * 60 * 1000, + reuseExistingServer: !process.env.CI, + stdout: "pipe", + }, { cwd: path.join(process.cwd(), "wasm-split-harness"), command: