diff --git a/Cargo.lock b/Cargo.lock index 0e1f2448..da9b2bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,7 +212,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-barcode-scanner", "tauri-plugin-biometric", "tauri-plugin-cli", @@ -243,7 +243,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-updater", "time", "tiny_http", @@ -256,7 +256,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-updater", "tiny_http", ] @@ -268,7 +268,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-store", ] @@ -291,9 +291,9 @@ dependencies = [ "core-graphics 0.23.2", "image", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "parking_lot", "windows-sys 0.48.0", "x11rb", @@ -701,7 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f" dependencies = [ "block-sys", - "objc2", + "objc2 0.5.2", ] [[package]] @@ -710,7 +710,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +dependencies = [ + "objc2 0.6.0", ] [[package]] @@ -903,6 +912,16 @@ dependencies = [ "toml 0.8.19", ] +[[package]] +name = "cargo_toml" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" +dependencies = [ + "serde", + "toml 0.8.19", +] + [[package]] name = "cc" version = "1.2.2" @@ -1041,36 +1060,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cocoa" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" -dependencies = [ - "bitflags 2.7.0", - "block", - "cocoa-foundation", - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "foreign-types 0.5.0", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" -dependencies = [ - "bitflags 2.7.0", - "block", - "core-foundation 0.10.0", - "core-graphics-types 0.2.0", - "libc", - "objc", -] - [[package]] name = "color-backtrace" version = "0.6.1" @@ -1492,7 +1481,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-deep-link", "tauri-plugin-log", "tauri-plugin-single-instance", @@ -1833,6 +1822,20 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "embed-resource" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbc6e0d8e0c03a655b53ca813f0463d2c956bc4db8138dbc89f120b066551e3" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.19", + "vswhom", + "winreg 0.52.0", +] + [[package]] name = "embed_plist" version = "1.2.2" @@ -2509,8 +2512,8 @@ checksum = "b00d88f1be7bf4cd2e61623ce08e84be2dfa4eab458e5d632d3dab95f16c1f64" dependencies = [ "crossbeam-channel", "keyboard-types", - "objc2", - "objc2-app-kit", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", "once_cell", "serde", "thiserror 1.0.69", @@ -2870,6 +2873,16 @@ dependencies = [ "png", ] +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + [[package]] name = "icrate" version = "0.1.2" @@ -2877,7 +2890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642" dependencies = [ "block2 0.4.0", - "objc2", + "objc2 0.5.2", ] [[package]] @@ -3069,6 +3082,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "inotify" version = "0.11.0" @@ -3675,21 +3697,22 @@ dependencies = [ [[package]] name = "muda" -version = "0.15.3" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", "once_cell", "png", "serde", - "thiserror 1.0.69", + "thiserror 2.0.9", "windows-sys 0.59.0", ] @@ -3949,9 +3972,6 @@ name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" -dependencies = [ - "cc", -] [[package]] name = "objc2" @@ -3963,6 +3983,16 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -3972,35 +4002,41 @@ dependencies = [ "bitflags 2.7.0", "block2 0.5.1", "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", ] [[package]] -name = "objc2-cloud-kit" -version = "0.2.2" +name = "objc2-app-kit" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ "bitflags 2.7.0", - "block2 0.5.1", - "objc2", - "objc2-core-location", - "objc2-foundation", + "block2 0.6.0", + "libc", + "objc2 0.6.0", + "objc2-cloud-kit", + "objc2-core-data 0.3.0", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.0", + "objc2-foundation 0.3.0", + "objc2-quartz-core 0.3.0", ] [[package]] -name = "objc2-contacts" -version = "0.2.2" +name = "objc2-cloud-kit" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" dependencies = [ - "block2 0.5.1", - "objc2", - "objc2-foundation", + "bitflags 2.7.0", + "objc2 0.6.0", + "objc2-foundation 0.3.0", ] [[package]] @@ -4011,8 +4047,41 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.7.0", "block2 0.5.1", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.0", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.0", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.0", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -4022,28 +4091,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] [[package]] -name = "objc2-core-location" -version = "0.2.2" +name = "objc2-core-image" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" dependencies = [ - "block2 0.5.1", - "objc2", - "objc2-contacts", - "objc2-foundation", + "objc2 0.6.0", + "objc2-foundation 0.3.0", ] [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] [[package]] name = "objc2-foundation" @@ -4055,19 +4131,31 @@ dependencies = [ "block2 0.5.1", "dispatch", "libc", - "objc2", + "objc2 0.5.2", ] [[package]] -name = "objc2-link-presentation" -version = "0.2.2" +name = "objc2-foundation" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ - "block2 0.5.1", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "bitflags 2.7.0", + "block2 0.6.0", + "libc", + "objc2 0.6.0", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.0", + "objc2-core-foundation", ] [[package]] @@ -4078,8 +4166,8 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.7.0", "block2 0.5.1", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -4089,9 +4177,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6788b04a18ea31e3dc3ab256b8546639e5bbae07c1a0dc4ea8615252bc6aee9a" dependencies = [ "bitflags 2.7.0", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -4102,77 +4190,46 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.7.0", "block2 0.5.1", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] [[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" +name = "objc2-quartz-core" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" dependencies = [ "bitflags 2.7.0", - "block2 0.5.1", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", + "objc2 0.6.0", + "objc2-foundation 0.3.0", ] [[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2 0.5.1", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" +name = "objc2-ui-kit" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" dependencies = [ "bitflags 2.7.0", - "block2 0.5.1", - "objc2", - "objc2-core-location", - "objc2-foundation", + "objc2 0.6.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", ] [[package]] name = "objc2-web-kit" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" +checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" dependencies = [ "bitflags 2.7.0", - "block2 0.5.1", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "block2 0.6.0", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", ] [[package]] @@ -4324,7 +4381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35366a452fce3f8947eb2f33226a133aaf0cacedef2af67ade348d58be7f85d0" dependencies = [ "icrate", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-osa-kit", "serde", "serde_json", @@ -5141,9 +5198,9 @@ dependencies = [ "gtk-sys", "js-sys", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -5770,7 +5827,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-cli", "tauri-plugin-single-instance", ] @@ -5821,9 +5878,9 @@ dependencies = [ "foreign-types 0.5.0", "js-sys", "log", - "objc2", - "objc2-foundation", - "objc2-quartz-core", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", "raw-window-handle", "redox_syscall", "wasm-bindgen", @@ -6327,12 +6384,11 @@ dependencies = [ [[package]] name = "tao" -version = "0.31.1" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3731d04d4ac210cd5f344087733943b9bfb1a32654387dad4d1c70de21aee2c9" +checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1" dependencies = [ "bitflags 2.7.0", - "cocoa", "core-foundation 0.10.0", "core-graphics 0.24.0", "crossbeam-channel", @@ -6349,7 +6405,9 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys", - "objc", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-foundation 0.3.0", "once_cell", "parking_lot", "raw-window-handle", @@ -6357,8 +6415,8 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.58.0", - "windows-core 0.58.0", + "windows 0.60.0", + "windows-core 0.60.1", "windows-version", "x11-dl", ] @@ -6399,13 +6457,11 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2979ec5ec5a9310b15d1548db3b8de98d8f75abf2b5b00fec9cd5c0553ecc09c" +version = "2.3.1" dependencies = [ "anyhow", "bytes", - "dirs 5.0.1", + "dirs 6.0.0", "dunce", "embed_plist", "futures-util", @@ -6421,9 +6477,9 @@ dependencies = [ "log", "mime", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-foundation 0.3.0", "percent-encoding", "plist", "raw-window-handle", @@ -6434,11 +6490,11 @@ dependencies = [ "serialize-to-javascript", "specta", "swift-rs", - "tauri-build", + "tauri-build 2.0.6", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", - "tauri-utils", + "tauri-utils 2.2.0", "thiserror 2.0.9", "tokio", "tray-icon", @@ -6448,7 +6504,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.58.0", + "windows 0.60.0", ] [[package]] @@ -6458,7 +6514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e950124f6779c6cf98e3260c7a6c8488a74aa6350dd54c6950fdaa349bca2df" dependencies = [ "anyhow", - "cargo_toml", + "cargo_toml 0.21.0", "dirs 5.0.1", "glob", "heck 0.5.0", @@ -6468,9 +6524,29 @@ dependencies = [ "semver", "serde", "serde_json", - "tauri-codegen", - "tauri-utils", - "tauri-winres", + "tauri-codegen 2.0.4", + "tauri-utils 2.1.1", + "tauri-winres 0.1.1", + "toml 0.8.19", + "walkdir", +] + +[[package]] +name = "tauri-build" +version = "2.0.6" +dependencies = [ + "anyhow", + "cargo_toml 0.22.1", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars", + "semver", + "serde", + "serde_json", + "tauri-utils 2.2.0", + "tauri-winres 0.3.0", "toml 0.8.19", "walkdir", ] @@ -6480,10 +6556,34 @@ name = "tauri-codegen" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f77894f9ddb5cb6c04fcfe8c8869ebe0aded4dabf19917118d48be4a95599ab5" +dependencies = [ + "base64 0.22.1", + "ico 0.3.0", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.90", + "tauri-utils 2.1.1", + "thiserror 2.0.9", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.0.5" dependencies = [ "base64 0.22.1", "brotli", - "ico", + "ico 0.4.0", "json-patch", "plist", "png", @@ -6494,7 +6594,7 @@ dependencies = [ "serde_json", "sha2", "syn 2.0.90", - "tauri-utils", + "tauri-utils 2.2.0", "thiserror 2.0.9", "time", "url", @@ -6504,16 +6604,14 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3240a5caed760a532e8f687be6f05b2c7d11a1d791fb53ccc08cfeb3e5308736" +version = "2.0.5" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.90", - "tauri-codegen", - "tauri-utils", + "tauri-codegen 2.0.5", + "tauri-utils 2.2.0", ] [[package]] @@ -6528,7 +6626,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "tauri-utils", + "tauri-utils 2.1.1", "toml 0.8.19", "walkdir", ] @@ -6606,12 +6704,12 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "tauri-utils", + "tauri-utils 2.1.1", "thiserror 2.0.9", "tracing", "url", "windows-registry 0.4.0", - "windows-result 0.3.0", + "windows-result 0.3.2", ] [[package]] @@ -6646,7 +6744,7 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "tauri-utils", + "tauri-utils 2.1.1", "thiserror 2.0.9", "toml 0.8.19", "url", @@ -6734,8 +6832,8 @@ dependencies = [ "byte-unit", "fern", "log", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "serde", "serde_json", "serde_repr", @@ -6787,8 +6885,8 @@ version = "2.2.5" dependencies = [ "dunce", "glob", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "open", "schemars", "serde", @@ -6947,7 +7045,7 @@ dependencies = [ "flate2", "futures-util", "http", - "infer", + "infer 0.16.0", "minisign-verify", "osakit", "percent-encoding", @@ -7017,10 +7115,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2274ef891ccc0a8d318deffa9d70053f947664d12d58b9c0d1ae5e89237e01f7" +version = "2.4.0" dependencies = [ + "cookie", "dpi", "gtk", "http", @@ -7028,35 +7125,34 @@ dependencies = [ "raw-window-handle", "serde", "serde_json", - "tauri-utils", + "tauri-utils 2.2.0", "thiserror 2.0.9", "url", - "windows 0.58.0", + "windows 0.60.0", ] [[package]] name = "tauri-runtime-wry" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3707b40711d3b9f6519150869e358ffbde7c57567fb9b5a8b51150606939b2a0" +version = "2.4.1" dependencies = [ "gtk", "http", "jni", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-foundation 0.3.0", + "once_cell", "percent-encoding", "raw-window-handle", "softbuffer", "tao", "tauri-runtime", - "tauri-utils", + "tauri-utils 2.2.0", "url", "webkit2gtk", "webview2-com", - "windows 0.58.0", + "windows 0.60.0", "wry", ] @@ -7067,6 +7163,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96fb10e7cc97456b2d5b9c03e335b5de5da982039a303a20d10006885e4523a0" dependencies = [ "aes-gcm", + "cargo_metadata", + "ctor", + "dunce", + "getrandom 0.2.15", + "glob", + "html5ever", + "http", + "infer 0.16.0", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "regex", + "schemars", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "serialize-to-javascript", + "swift-rs", + "thiserror 2.0.9", + "toml 0.8.19", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-utils" +version = "2.2.0" +dependencies = [ + "aes-gcm", + "anyhow", "brotli", "cargo_metadata", "ctor", @@ -7075,7 +7209,7 @@ dependencies = [ "glob", "html5ever", "http", - "infer", + "infer 0.19.0", "json-patch", "kuchikiki", "log", @@ -7106,10 +7240,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" dependencies = [ - "embed-resource", + "embed-resource 2.5.1", "toml 0.7.8", ] +[[package]] +name = "tauri-winres" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56eaa45f707bedf34d19312c26d350bc0f3c59a47e58e8adbeecdc850d2c13a0" +dependencies = [ + "embed-resource 3.0.2", + "toml 0.8.19", +] + [[package]] name = "tauri-winrt-notification" version = "0.2.1" @@ -7506,22 +7650,23 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" +checksum = "d433764348e7084bad2c5ea22c96c71b61b17afe3a11645710f533bd72b6a2b5" dependencies = [ - "core-graphics 0.24.0", "crossbeam-channel", - "dirs 5.0.1", + "dirs 6.0.0", "libappindicator", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.0", "once_cell", "png", "serde", - "thiserror 1.0.69", + "thiserror 2.0.9", "windows-sys 0.59.0", ] @@ -7690,7 +7835,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-updater", "time", "tiny_http", @@ -8084,7 +8229,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.0.5", "tauri-plugin-websocket", "tokio", "tokio-tungstenite", @@ -8092,16 +8237,16 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.34.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823e7ebcfaea51e78f72c87fc3b65a1e602c321f407a0b36dbb327d7bb7cd921" +checksum = "b0d606f600e5272b514dbb66539dd068211cc20155be8d3958201b4b5bd79ed3" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.58.0", - "windows-core 0.58.0", - "windows-implement 0.58.0", - "windows-interface 0.58.0", + "windows 0.60.0", + "windows-core 0.60.1", + "windows-implement 0.59.0", + "windows-interface 0.59.1", ] [[package]] @@ -8117,13 +8262,13 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.34.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a82bce72db6e5ee83c68b5de1e2cd6ea195b9fbff91cb37df5884cbe3222df4" +checksum = "bfb27fccd3c27f68e9a6af1bcf48c2d82534b8675b83608a4d81446d095a17ac" dependencies = [ - "thiserror 1.0.69", - "windows 0.58.0", - "windows-core 0.58.0", + "thiserror 2.0.9", + "windows 0.60.0", + "windows-core 0.60.1", ] [[package]] @@ -8185,13 +8330,14 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window-vibrancy" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea403deff7b51fff19e261330f71608ff2cdef5721d72b64180bb95be7c4150" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -8230,6 +8376,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +dependencies = [ + "windows-collections", + "windows-core 0.60.1", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +dependencies = [ + "windows-core 0.60.1", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -8264,6 +8432,29 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.3.1", +] + +[[package]] +name = "windows-future" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + [[package]] name = "windows-implement" version = "0.56.0" @@ -8286,6 +8477,17 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "windows-interface" version = "0.56.0" @@ -8308,6 +8510,33 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + [[package]] name = "windows-registry" version = "0.2.0" @@ -8325,8 +8554,8 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.3.0", - "windows-strings 0.3.0", + "windows-result 0.3.2", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -8350,11 +8579,11 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-targets 0.53.0", + "windows-link", ] [[package]] @@ -8369,11 +8598,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-targets 0.53.0", + "windows-link", ] [[package]] @@ -8744,12 +8973,12 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.48.0" +version = "0.50.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e644bf458e27b11b0ecafc9e5633d1304fdae82baca1d42185669752fe6ca4f" +checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730" dependencies = [ "base64 0.22.1", - "block2 0.5.1", + "block2 0.6.0", "cookie", "crossbeam-channel", "dpi", @@ -8763,9 +8992,10 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -8779,8 +9009,8 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.58.0", - "windows-core 0.58.0", + "windows 0.60.0", + "windows-core 0.60.1", "windows-version", "x11-dl", ] diff --git a/Cargo.toml b/Cargo.toml index d85be889..afcccb69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,41 +1,41 @@ -[workspace] -members = [ - "plugins/*", - "plugins/*/tests/*", - "plugins/updater/tests/updater-migration/v2-app", - "plugins/*/examples/*/src-tauri", - "examples/*/src-tauri", -] -resolver = "2" - -[workspace.dependencies] -serde = { version = "1", features = ["derive"] } -tracing = "0.1" -log = "0.4" -tauri = { version = "2", default-features = false } -tauri-build = "2" -tauri-plugin = "2" -tauri-utils = "2" -serde_json = "1" -thiserror = "2" -url = "2" -schemars = "0.8" -dunce = "1" -specta = "^2.0.0-rc.16" -glob = "0.3" -zbus = "5" - -[workspace.package] -edition = "2021" -authors = ["Tauri Programme within The Commons Conservancy"] -license = "Apache-2.0 OR MIT" -rust-version = "1.77.2" -repository = "https://github.com/tauri-apps/plugins-workspace" - -# default to small, optimized release binaries -[profile.release] -panic = "abort" -codegen-units = 1 -lto = true -incremental = false -opt-level = "s" +[workspace] +members = [ + "plugins/*", + "plugins/*/tests/*", + "plugins/updater/tests/updater-migration/v2-app", + "plugins/*/examples/*/src-tauri", + "examples/*/src-tauri", +] +resolver = "2" + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +tracing = "0.1" +log = "0.4" +tauri = { path = "../tauri/crates/tauri", default-features = false } +tauri-build = "2" +tauri-plugin = "2" +tauri-utils = "2" +serde_json = "1" +thiserror = "2" +url = "2" +schemars = "0.8" +dunce = "1" +specta = "^2.0.0-rc.16" +glob = "0.3" +zbus = "5" + +[workspace.package] +edition = "2021" +authors = ["Tauri Programme within The Commons Conservancy"] +license = "Apache-2.0 OR MIT" +rust-version = "1.77.2" +repository = "https://github.com/tauri-apps/plugins-workspace" + +# default to small, optimized release binaries +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +incremental = false +opt-level = "s" diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index d7a2e547..fd62c8bf 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -1,1434 +1,1434 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - collections::HashMap, - ffi::{OsStr, OsString}, - io::Cursor, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, - time::Duration, -}; - -use base64::Engine; -use futures_util::StreamExt; -use http::HeaderName; -use minisign_verify::{PublicKey, Signature}; -use percent_encoding::{AsciiSet, CONTROLS}; -use reqwest::{ - header::{HeaderMap, HeaderValue}, - ClientBuilder, StatusCode, -}; -use semver::Version; -use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; -use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime}; -use time::OffsetDateTime; -use url::Url; - -use crate::{ - error::{Error, Result}, - Config, -}; - -const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); - -#[derive(Clone)] -pub enum Installer { - AppImage, - Deb, - Rpm, - - App, - - Msi, - Nsis, -} - -impl Installer{ - fn suffix(self) -> &'static str { - match self { - Self::AppImage => "appimage", - Self::Deb => "deb", - Self::Rpm => "rpm", - Self::App => "app", - Self::Msi => "msi", - Self::Nsis => "nsis", - } - } -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ReleaseManifestPlatform { - /// Download URL for the platform - pub url: Url, - /// Signature for the platform - pub signature: String, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(untagged)] -pub enum RemoteReleaseInner { - Dynamic(ReleaseManifestPlatform), - Static { - platforms: HashMap, - }, -} - -/// Information about a release returned by the remote update server. -/// -/// This type can have one of two shapes: Server Format (Dynamic Format) and Static Format. -#[derive(Debug, Clone)] -pub struct RemoteRelease { - /// Version to install. - pub version: Version, - /// Release notes. - pub notes: Option, - /// Release date. - pub pub_date: Option, - /// Release data. - pub data: RemoteReleaseInner, -} - -impl RemoteRelease { - /// The release's download URL for the given target. - pub fn download_url(&self, target: &str, installer: Option) -> Result<&Url> { - - let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); - match self.data { - RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url), - RemoteReleaseInner::Static { ref platforms } => platforms - .get(target) - .map_or_else( - || match fallback_target { - Some(fallback) => platforms.get(&fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback)), |p| Ok(&p.url)), - None => Err(Error::TargetNotFound(target.to_string())) - }, |p| { Ok(&p.url) }) - } - } - - /// The release's signature for the given target. - pub fn signature(&self, target: &str, installer: Option) -> Result<&String> { - let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); - - match self.data { - RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature), - RemoteReleaseInner::Static { ref platforms } => platforms - .get(target) - .map_or_else( - || match fallback_target { - Some(fallback) => platforms.get(&fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback)), |p| Ok(&p.signature)), - None => Err(Error::TargetNotFound(target.to_string())) - }, |p| { Ok(&p.signature) }) - } - } -} - -pub type OnBeforeExit = Arc; -pub type VersionComparator = Arc bool + Send + Sync>; -type MainThreadClosure = Box; -type RunOnMainThread = - Box std::result::Result<(), tauri::Error> + Send + Sync + 'static>; - -pub struct UpdaterBuilder { - #[allow(dead_code)] - run_on_main_thread: RunOnMainThread, - app_name: String, - current_version: Version, - config: Config, - pub(crate) version_comparator: Option, - executable_path: Option, - target: Option, - endpoints: Option>, - headers: HeaderMap, - timeout: Option, - proxy: Option, - installer_args: Vec, - current_exe_args: Vec, - on_before_exit: Option, -} - -impl UpdaterBuilder { - pub(crate) fn new(app: &AppHandle, config: crate::Config) -> Self { - let app_ = app.clone(); - let run_on_main_thread = - move |f: Box| app_.run_on_main_thread(f); - Self { - run_on_main_thread: Box::new(run_on_main_thread), - installer_args: config - .windows - .as_ref() - .map(|w| w.installer_args.clone()) - .unwrap_or_default(), - current_exe_args: Vec::new(), - app_name: app.package_info().name.clone(), - current_version: app.package_info().version.clone(), - config, - version_comparator: None, - executable_path: None, - target: None, - endpoints: None, - headers: Default::default(), - timeout: None, - proxy: None, - on_before_exit: None, - } - } - - pub fn version_comparator bool + Send + Sync + 'static>( - mut self, - f: F, - ) -> Self { - self.version_comparator = Some(Arc::new(f)); - self - } - - pub fn target(mut self, target: impl Into) -> Self { - self.target.replace(target.into()); - self - } - - pub fn endpoints(mut self, endpoints: Vec) -> Result { - crate::config::validate_endpoints( - &endpoints, - self.config.dangerous_insecure_transport_protocol, - )?; - - self.endpoints.replace(endpoints); - Ok(self) - } - - pub fn executable_path>(mut self, p: P) -> Self { - self.executable_path.replace(p.as_ref().into()); - self - } - - pub fn header(mut self, key: K, value: V) -> Result - where - HeaderName: TryFrom, - >::Error: Into, - HeaderValue: TryFrom, - >::Error: Into, - { - let key: std::result::Result = key.try_into().map_err(Into::into); - let value: std::result::Result = - value.try_into().map_err(Into::into); - self.headers.insert(key?, value?); - - Ok(self) - } - - pub fn headers(mut self, headers: HeaderMap) -> Self { - self.headers = headers; - self - } - - pub fn clear_headers(mut self) -> Self { - self.headers.clear(); - self - } - - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - pub fn proxy(mut self, proxy: Url) -> Self { - self.proxy.replace(proxy); - self - } - - pub fn pubkey>(mut self, pubkey: S) -> Self { - self.config.pubkey = pubkey.into(); - self - } - - pub fn installer_arg(mut self, arg: S) -> Self - where - S: Into, - { - self.installer_args.push(arg.into()); - self - } - - pub fn installer_args(mut self, args: I) -> Self - where - I: IntoIterator, - S: Into, - { - let args = args.into_iter().map(|a| a.into()).collect::>(); - self.installer_args.extend_from_slice(&args); - self - } - - pub fn clear_installer_args(mut self) -> Self { - self.installer_args.clear(); - self - } - - pub fn on_before_exit(mut self, f: F) -> Self { - self.on_before_exit.replace(Arc::new(f)); - self - } - - pub fn build(self) -> Result { - let endpoints = self - .endpoints - .unwrap_or_else(|| self.config.endpoints.clone()); - - if endpoints.is_empty() { - return Err(Error::EmptyEndpoints); - }; - - let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?; - let (target, json_target) = if let Some(target) = self.target { - (target.clone(), target ) - } else { - let target = get_updater_target().ok_or(Error::UnsupportedOs)?; - let json_target = format!("{target}-{arch}"); - (target.to_owned(), json_target) - }; - - let executable_path = self.executable_path.clone().unwrap_or(current_exe()?); - - // Get the extract_path from the provided executable_path - let extract_path = if cfg!(target_os = "linux") { - executable_path - } else { - extract_path_from_executable(&executable_path)? - }; - - Ok(Updater { - run_on_main_thread: Arc::new(self.run_on_main_thread), - config: self.config, - app_name: self.app_name, - current_version: self.current_version, - version_comparator: self.version_comparator, - timeout: self.timeout, - proxy: self.proxy, - endpoints, - installer_args: self.installer_args, - current_exe_args: self.current_exe_args, - arch, - target, - json_target, - headers: self.headers, - extract_path, - on_before_exit: self.on_before_exit, - }) - } -} - -impl UpdaterBuilder { - pub(crate) fn current_exe_args(mut self, args: I) -> Self - where - I: IntoIterator, - S: Into, - { - let args = args.into_iter().map(|a| a.into()).collect::>(); - self.current_exe_args.extend_from_slice(&args); - self - } -} - -pub struct Updater { - #[allow(dead_code)] - run_on_main_thread: Arc, - config: Config, - app_name: String, - current_version: Version, - version_comparator: Option, - timeout: Option, - proxy: Option, - endpoints: Vec, - arch: &'static str, - // The `{{target}}` variable we replace in the endpoint and serach for in the JSON - target: String, - // The value we search if the updater server returns a JSON with the `platforms` object - json_target: String, - headers: HeaderMap, - extract_path: PathBuf, - on_before_exit: Option, - #[allow(unused)] - installer_args: Vec, - #[allow(unused)] - current_exe_args: Vec, -} - - - - -impl Updater { - - fn get_updater_installer(&self) -> Result> { - match tauri::__TAURI_BUNDLE_TYPE { - "DEB_BUNDLE" => Ok(Some(Installer::Deb)), - "RPM_BUNDLE" => Ok(Some(Installer::Rpm)), - "APP_BUNDLE" => Ok(Some(Installer::AppImage)), - "MSI_BUNDLE" => Ok(Some(Installer::Msi)), - "NSS_BUNDLE" => Ok(Some(Installer::Nsis)), - _ => Err(Error::UnknownInstaller) - } - } - - pub async fn check(&self) -> Result> { - // we want JSON only - let mut headers = self.headers.clone(); - headers.insert("Accept", HeaderValue::from_str("application/json").unwrap()); - // Set SSL certs for linux if they aren't available. - #[cfg(target_os = "linux")] - { - if std::env::var_os("SSL_CERT_FILE").is_none() { - std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); - } - if std::env::var_os("SSL_CERT_DIR").is_none() { - std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); - } - } - - let mut remote_release: Option = None; - let mut raw_json: Option = None; - let mut last_error: Option = None; - for url in &self.endpoints { - // replace {{current_version}}, {{target}} and {{arch}} in the provided URL - // this is useful if we need to query example - // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}} - // will be translated into -> - // https://releases.myapp.com/update/darwin/aarch64/1.0.0 - // The main objective is if the update URL is defined via the Cargo.toml - // the URL will be generated dynamically - let version = self.current_version.to_string(); - let version = version.as_bytes(); - const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+'); - let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD); - let encoded_version = encoded_version.to_string(); - - let url: Url = url - .to_string() - // url::Url automatically url-encodes the path components - .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version) - .replace("%7B%7Btarget%7D%7D", &self.target) - .replace("%7B%7Barch%7D%7D", self.arch) - // but not query parameters - .replace("{{current_version}}", &encoded_version) - .replace("{{target}}", &self.target) - .replace("{{arch}}", self.arch) - .parse()?; - - let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); - if let Some(timeout) = self.timeout { - request = request.timeout(timeout); - } - if let Some(ref proxy) = self.proxy { - let proxy = reqwest::Proxy::all(proxy.as_str())?; - request = request.proxy(proxy); - } - let response = request - .build()? - .get(url) - .headers(headers.clone()) - .send() - .await; - - match response { - Ok(res) => { - if res.status().is_success() { - // no updates found! - if StatusCode::NO_CONTENT == res.status() { - return Ok(None); - }; - - raw_json = Some(res.json().await?); - match serde_json::from_value::(raw_json.clone().unwrap()) - .map_err(Into::into) - { - Ok(release) => { - last_error = None; - remote_release = Some(release); - // we found a relase, break the loop - break; - } - Err(err) => last_error = Some(err), - } - } - } - Err(err) => last_error = Some(err.into()), - } - } - - // Last error is cleaned on success. - // Shouldn't be triggered if we had a successfull call - if let Some(error) = last_error { - return Err(error); - } - - // Extracted remote metadata - let release = remote_release.ok_or(Error::ReleaseNotFound)?; - - let should_update = match self.version_comparator.as_ref() { - Some(comparator) => comparator(self.current_version.clone(), release.clone()), - None => release.version > self.current_version, - }; - - let installer = self.get_updater_installer()?; - - let update = if should_update { - Some(Update { - run_on_main_thread: self.run_on_main_thread.clone(), - config: self.config.clone(), - on_before_exit: self.on_before_exit.clone(), - app_name: self.app_name.clone(), - current_version: self.current_version.to_string(), - target: self.target.clone(), - extract_path: self.extract_path.clone(), - version: release.version.to_string(), - date: release.pub_date, - download_url: release.download_url(&self.json_target, installer.clone())?.to_owned(), - body: release.notes.clone(), - signature: release.signature(&self.json_target, installer.clone())?.to_owned(), - installer, - raw_json: raw_json.unwrap(), - timeout: self.timeout, - proxy: self.proxy.clone(), - headers: self.headers.clone(), - installer_args: self.installer_args.clone(), - current_exe_args: self.current_exe_args.clone(), - }) - } else { - None - }; - - Ok(update) - } -} - -#[derive(Clone)] -pub struct Update { - #[allow(dead_code)] - run_on_main_thread: Arc, - config: Config, - #[allow(unused)] - on_before_exit: Option, - /// Update description - pub body: Option, - /// Version used to check for update - pub current_version: String, - /// Version announced - pub version: String, - /// Update publish date - pub date: Option, - /// Target - pub target: String, - /// Current installer - pub installer: Option, - /// Download URL announced - pub download_url: Url, - /// Signature announced - pub signature: String, - /// The raw version of server's JSON response. Useful if the response contains additional fields that the updater doesn't handle. - pub raw_json: serde_json::Value, - /// Request timeout - pub timeout: Option, - /// Request proxy - pub proxy: Option, - /// Request headers - pub headers: HeaderMap, - /// Extract path - #[allow(unused)] - extract_path: PathBuf, - /// App name, used for creating named tempfiles on Windows - #[allow(unused)] - app_name: String, - #[allow(unused)] - installer_args: Vec, - #[allow(unused)] - current_exe_args: Vec, -} - -impl Resource for Update {} - -impl Update { - /// Downloads the updater package, verifies it then return it as bytes. - /// - /// Use [`Update::install`] to install it - pub async fn download), D: FnOnce()>( - &self, - mut on_chunk: C, - on_download_finish: D, - ) -> Result> { - // set our headers - let mut headers = self.headers.clone(); - headers.insert( - "Accept", - HeaderValue::from_str("application/octet-stream").unwrap(), - ); - - let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); - if let Some(timeout) = self.timeout { - request = request.timeout(timeout); - } - if let Some(ref proxy) = self.proxy { - let proxy = reqwest::Proxy::all(proxy.as_str())?; - request = request.proxy(proxy); - } - let response = request - .build()? - .get(self.download_url.clone()) - .headers(headers) - .send() - .await?; - - if !response.status().is_success() { - return Err(Error::Network(format!( - "Download request failed with status: {}", - response.status() - ))); - } - - let content_length: Option = response - .headers() - .get("Content-Length") - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.parse().ok()); - - let mut buffer = Vec::new(); - - let mut stream = response.bytes_stream(); - while let Some(chunk) = stream.next().await { - let chunk = chunk?; - on_chunk(chunk.len(), content_length); - buffer.extend(chunk); - } - on_download_finish(); - - verify_signature(&buffer, &self.signature, &self.config.pubkey)?; - - Ok(buffer) - } - - /// Installs the updater package downloaded by [`Update::download`] - pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> { - self.install_inner(bytes.as_ref()) - } - - /// Downloads and installs the updater package - pub async fn download_and_install), D: FnOnce()>( - &self, - on_chunk: C, - on_download_finish: D, - ) -> Result<()> { - let bytes = self.download(on_chunk, on_download_finish).await?; - self.install(bytes) - } - - #[cfg(mobile)] - fn install_inner(&self, _bytes: &[u8]) -> Result<()> { - Ok(()) - } -} - -#[cfg(windows)] -enum WindowsUpdaterType { - Nsis { - path: PathBuf, - #[allow(unused)] - temp: Option, - }, - Msi { - path: PathBuf, - #[allow(unused)] - temp: Option, - }, -} - -#[cfg(windows)] -impl WindowsUpdaterType { - fn nsis(path: PathBuf, temp: Option) -> Self { - Self::Nsis { path, temp } - } - - fn msi(path: PathBuf, temp: Option) -> Self { - Self::Msi { - path: path.wrap_in_quotes(), - temp, - } - } -} - -#[cfg(windows)] -impl Config { - fn install_mode(&self) -> crate::config::WindowsUpdateInstallMode { - self.windows - .as_ref() - .map(|w| w.install_mode.clone()) - .unwrap_or_default() - } -} - -/// Windows -#[cfg(windows)] -impl Update { - /// ### Expected structure: - /// ├── [AppName]_[version]_x64.msi # Application MSI - /// ├── [AppName]_[version]_x64-setup.exe # NSIS installer - /// ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler - /// │ └──[AppName]_[version]_x64.msi # Application MSI - /// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler - /// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer - /// └── ... - fn install_inner(&self, bytes: &[u8]) -> Result<()> { - use std::iter::once; - use windows_sys::{ - w, - Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW}, - }; - - let updater_type = self.extract(bytes)?; - - let install_mode = self.config.install_mode(); - let current_args = &self.current_exe_args()[1..]; - let msi_args; - - let installer_args: Vec<&OsStr> = match &updater_type { - WindowsUpdaterType::Nsis { .. } => install_mode - .nsis_args() - .iter() - .map(OsStr::new) - .chain(once(OsStr::new("/UPDATE"))) - .chain(once(OsStr::new("/ARGS"))) - .chain(current_args.to_vec()) - .chain(self.installer_args()) - .collect(), - WindowsUpdaterType::Msi { path, .. } => { - let escaped_args = current_args - .iter() - .map(escape_msi_property_arg) - .collect::>() - .join(" "); - msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\"")); - - [OsStr::new("/i"), path.as_os_str()] - .into_iter() - .chain(install_mode.msiexec_args().iter().map(OsStr::new)) - .chain(once(OsStr::new("/promptrestart"))) - .chain(self.installer_args()) - .chain(once(OsStr::new("AUTOLAUNCHAPP=True"))) - .chain(once(msi_args.as_os_str())) - .collect() - } - }; - - if let Some(on_before_exit) = self.on_before_exit.as_ref() { - on_before_exit(); - } - - let file = match &updater_type { - WindowsUpdaterType::Nsis { path, .. } => path.as_os_str().to_os_string(), - WindowsUpdaterType::Msi { .. } => std::env::var("SYSTEMROOT").as_ref().map_or_else( - |_| OsString::from("msiexec.exe"), - |p| OsString::from(format!("{p}\\System32\\msiexec.exe")), - ), - }; - let file = encode_wide(file); - - let parameters = installer_args.join(OsStr::new(" ")); - let parameters = encode_wide(parameters); - - unsafe { - ShellExecuteW( - std::ptr::null_mut(), - w!("open"), - file.as_ptr(), - parameters.as_ptr(), - std::ptr::null(), - SW_SHOW, - ) - }; - - std::process::exit(0); - } - - fn installer_args(&self) -> Vec<&OsStr> { - self.installer_args - .iter() - .map(OsStr::new) - .collect::>() - } - - fn current_exe_args(&self) -> Vec<&OsStr> { - self.current_exe_args - .iter() - .map(OsStr::new) - .collect::>() - } - - fn extract(&self, bytes: &[u8]) -> Result { - #[cfg(feature = "zip")] - if infer::archive::is_zip(bytes) { - return self.extract_zip(bytes); - } - - self.extract_exe(bytes) - } - - fn make_temp_dir(&self) -> Result { - Ok(tempfile::Builder::new() - .prefix(&format!("{}-{}-updater-", self.app_name, self.version)) - .tempdir()? - .into_path()) - } - - #[cfg(feature = "zip")] - fn extract_zip(&self, bytes: &[u8]) -> Result { - let temp_dir = self.make_temp_dir()?; - - let archive = Cursor::new(bytes); - let mut extractor = zip::ZipArchive::new(archive)?; - extractor.extract(&temp_dir)?; - - let paths = std::fs::read_dir(&temp_dir)?; - for path in paths { - let path = path?.path(); - let ext = path.extension(); - if ext == Some(OsStr::new("exe")) { - return Ok(WindowsUpdaterType::nsis(path, None)); - } else if ext == Some(OsStr::new("msi")) { - return Ok(WindowsUpdaterType::msi(path, None)); - } - } - - Err(crate::Error::BinaryNotFoundInArchive) - } - - fn extract_exe(&self, bytes: &[u8]) -> Result { - if infer::app::is_exe(bytes) { - let (path, temp) = self.write_to_temp(bytes, ".exe")?; - Ok(WindowsUpdaterType::nsis(path, temp)) - } else if infer::archive::is_msi(bytes) { - let (path, temp) = self.write_to_temp(bytes, ".msi")?; - Ok(WindowsUpdaterType::msi(path, temp)) - } else { - Err(crate::Error::InvalidUpdaterFormat) - } - } - - fn write_to_temp( - &self, - bytes: &[u8], - ext: &str, - ) -> Result<(PathBuf, Option)> { - use std::io::Write; - - let temp_dir = self.make_temp_dir()?; - let mut temp_file = tempfile::Builder::new() - .prefix(&format!("{}-{}-installer", self.app_name, self.version)) - .suffix(ext) - .rand_bytes(0) - .tempfile_in(temp_dir)?; - temp_file.write_all(bytes)?; - - let temp = temp_file.into_temp_path(); - Ok((temp.to_path_buf(), Some(temp))) - } -} - -/// Linux (AppImage and Deb) -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -impl Update { - /// ### Expected structure: - /// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler - /// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage - /// ├── [AppName]_[version]_amd64.deb # Debian package - /// └── ... - /// - fn install_inner(&self, bytes: &[u8]) -> Result<()> { - match self.installer { - Some(Installer::Deb) => self.install_deb(bytes), - Some(Installer::Rpm) => self.install_rpm(bytes), - _ =>self.install_appimage(bytes) - } - } - - fn install_appimage(&self, bytes: &[u8]) -> Result<()> { - use std::os::unix::fs::{MetadataExt, PermissionsExt}; - let extract_path_metadata = self.extract_path.metadata()?; - - let tmp_dir_locations = vec![ - Box::new(|| Some(std::env::temp_dir())) as Box Option>, - Box::new(dirs::cache_dir), - Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), - ]; - - for tmp_dir_location in tmp_dir_locations { - if let Some(tmp_dir_location) = tmp_dir_location() { - let tmp_dir = tempfile::Builder::new() - .prefix("tauri_current_app") - .tempdir_in(tmp_dir_location)?; - let tmp_dir_metadata = tmp_dir.path().metadata()?; - - if extract_path_metadata.dev() == tmp_dir_metadata.dev() { - let mut perms = tmp_dir_metadata.permissions(); - perms.set_mode(0o700); - std::fs::set_permissions(tmp_dir.path(), perms)?; - - let tmp_app_image = &tmp_dir.path().join("current_app.AppImage"); - - let permissions = std::fs::metadata(&self.extract_path)?.permissions(); - - // create a backup of our current app image - std::fs::rename(&self.extract_path, tmp_app_image)?; - - #[cfg(feature = "zip")] - if infer::archive::is_gz(bytes) { - // extract the buffer to the tmp_dir - // we extract our signed archive into our final directory without any temp file - let archive = Cursor::new(bytes); - let decoder = flate2::read::GzDecoder::new(archive); - let mut archive = tar::Archive::new(decoder); - for mut entry in archive.entries()?.flatten() { - if let Ok(path) = entry.path() { - if path.extension() == Some(OsStr::new("AppImage")) { - // if something went wrong during the extraction, we should restore previous app - if let Err(err) = entry.unpack(&self.extract_path) { - std::fs::rename(tmp_app_image, &self.extract_path)?; - return Err(err.into()); - } - // early finish we have everything we need here - return Ok(()); - } - } - } - // if we have not returned early we should restore the backup - std::fs::rename(tmp_app_image, &self.extract_path)?; - return Err(Error::BinaryNotFoundInArchive); - } - - return match std::fs::write(&self.extract_path, bytes) - .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions)) - { - Err(err) => { - // if something went wrong during the extraction, we should restore previous app - std::fs::rename(tmp_app_image, &self.extract_path)?; - Err(err.into()) - } - Ok(_) => Ok(()), - }; - } - } - } - - Err(Error::TempDirNotOnSameMountPoint) - } - - - fn install_deb(&self, bytes: &[u8]) -> Result<()> { - // First verify the bytes are actually a .deb package - if !infer::archive::is_deb(bytes) { - return Err(Error::InvalidUpdaterFormat); - } - - self.try_tmp_locations(bytes, "dpkg", "-i") - - } - - fn install_rpm(&self, bytes: &[u8]) -> Result<()> { - // First verify the bytes are actually a .rpm package - if !infer::archive::is_rpm(bytes) { - return Err(Error::InvalidUpdaterFormat); - } - self.try_tmp_locations(bytes, "rpm", "-U") - } - - fn try_tmp_locations(&self, bytes: &[u8], install_cmd: &str, install_arg: &str) -> Result<()> { - // Try different temp directories - let tmp_dir_locations = vec![ - Box::new(|| Some(std::env::temp_dir())) as Box Option>, - Box::new(dirs::cache_dir), - Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), - ]; - - // Try writing to multiple temp locations until one succeeds - for tmp_dir_location in tmp_dir_locations { - if let Some(path) = tmp_dir_location() { - if let Ok(tmp_dir) = tempfile::Builder::new() - .prefix("tauri_rpm_update") - .tempdir_in(path) - { - let pkg_path = tmp_dir.path().join("package.rpm"); - - // Try writing the .deb file - if std::fs::write(&pkg_path, bytes).is_ok() { - // If write succeeds, proceed with installation - return self.try_install_with_privileges(&pkg_path, install_cmd, install_arg); - } - // If write fails, continue to next temp location - } - } - } - - // If we get here, all temp locations failed - Err(Error::TempDirNotFound) - - } - - fn try_install_with_privileges(&self, pkg_path: &Path, install_cmd: &str, install_arg: &str) -> Result<()> { - // 1. First try using pkexec (graphical sudo prompt) - if let Ok(status) = std::process::Command::new("pkexec") - .arg(install_cmd) - .arg(install_arg) - .arg(pkg_path) - .status() - { - if status.success() { - return Ok(()); - } - } - - // 2. Try zenity or kdialog for a graphical sudo experience - if let Ok(password) = self.get_password_graphically() { - if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? { - return Ok(()); - } - } - - // 3. Final fallback: terminal sudo - let status = std::process::Command::new("sudo") - .arg(install_cmd) - .arg(install_arg) - .arg(pkg_path) - .status()?; - - if status.success() { - Ok(()) - } else { - Err(Error::PackageInstallFailed) - } - } - - fn get_password_graphically(&self) -> Result { - // Try zenity first - let zenity_result = std::process::Command::new("zenity") - .args([ - "--password", - "--title=Authentication Required", - "--text=Enter your password to install the update:", - ]) - .output(); - - if let Ok(output) = zenity_result { - if output.status.success() { - return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); - } - } - - // Fall back to kdialog if zenity fails or isn't available - let kdialog_result = std::process::Command::new("kdialog") - .args(["--password", "Enter your password to install the update:"]) - .output(); - - if let Ok(output) = kdialog_result { - if output.status.success() { - return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); - } - } - - Err(Error::AuthenticationFailed) - } - - fn install_with_sudo(&self, pkg_path: &Path, password: &str, install_cmd: &str, install_arg: &str) -> Result { - use std::io::Write; - use std::process::{Command, Stdio}; - - let mut child = Command::new("sudo") - .arg("-S") // read password from stdin - .arg(install_cmd) - .arg(install_arg) - .arg(pkg_path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - if let Some(mut stdin) = child.stdin.take() { - // Write password to stdin - writeln!(stdin, "{}", password)?; - } - - let status = child.wait()?; - Ok(status.success()) - } -} - -/// MacOS -#[cfg(target_os = "macos")] -impl Update { - /// ### Expected structure: - /// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler - /// │ └──[AppName].app # Main application - /// │ └── Contents # Application contents... - /// │ └── ... - /// └── ... - fn install_inner(&self, bytes: &[u8]) -> Result<()> { - use flate2::read::GzDecoder; - - let cursor = Cursor::new(bytes); - let mut extracted_files: Vec = Vec::new(); - - // Create temp directories for backup and extraction - let tmp_backup_dir = tempfile::Builder::new() - .prefix("tauri_current_app") - .tempdir()?; - - let tmp_extract_dir = tempfile::Builder::new() - .prefix("tauri_updated_app") - .tempdir()?; - - let decoder = GzDecoder::new(cursor); - let mut archive = tar::Archive::new(decoder); - - // Extract files to temporary directory - for entry in archive.entries()? { - let mut entry = entry?; - let collected_path: PathBuf = entry.path()?.iter().skip(1).collect(); - let extraction_path = tmp_extract_dir.path().join(&collected_path); - - // Ensure parent directories exist - if let Some(parent) = extraction_path.parent() { - std::fs::create_dir_all(parent)?; - } - - if let Err(err) = entry.unpack(&extraction_path) { - // Cleanup on error - std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); - return Err(err.into()); - } - extracted_files.push(extraction_path); - } - - // Try to move the current app to backup - let move_result = std::fs::rename( - &self.extract_path, - tmp_backup_dir.path().join("current_app"), - ); - let need_authorization = if let Err(err) = move_result { - if err.kind() == std::io::ErrorKind::PermissionDenied { - true - } else { - std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); - return Err(err.into()); - } - } else { - false - }; - - if need_authorization { - // Use AppleScript to perform moves with admin privileges - let apple_script = format!( - "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges", - src = self.extract_path.display(), - new = tmp_extract_dir.path().display() - ); - - let (tx, rx) = std::sync::mpsc::channel(); - let res = (self.run_on_main_thread)(Box::new(move || { - let mut script = - osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script); - script.compile().expect("invalid AppleScript"); - let r = script.execute(); - tx.send(r).unwrap(); - })); - let result = rx.recv().unwrap(); - - if res.is_err() || result.is_err() { - std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); - return Err(Error::Io(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "Failed to move the new app into place", - ))); - } - } else { - // Remove existing directory if it exists - if self.extract_path.exists() { - std::fs::remove_dir_all(&self.extract_path)?; - } - // Move the new app to the target path - std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?; - } - - let _ = std::process::Command::new("touch") - .arg(&self.extract_path) - .status(); - - Ok(()) - } -} - -/// Gets the target string used on the updater. -pub fn target() -> Option { - if let (Some(target), Some(arch)) = (get_updater_target(), get_updater_arch()) { - Some(format!("{target}-{arch}")) - } else { - None - } -} - -pub(crate) fn get_updater_target() -> Option<&'static str> { - if cfg!(target_os = "linux") { - Some("linux") - } else if cfg!(target_os = "macos") { - // TODO shouldn't this be macos instead? - Some("darwin") - } else if cfg!(target_os = "windows") { - Some("windows") - } else { - None - } -} - - -pub(crate) fn get_updater_arch() -> Option<&'static str> { - if cfg!(target_arch = "x86") { - Some("i686") - } else if cfg!(target_arch = "x86_64") { - Some("x86_64") - } else if cfg!(target_arch = "arm") { - Some("armv7") - } else if cfg!(target_arch = "aarch64") { - Some("aarch64") - } else { - None - } -} - - - -pub fn extract_path_from_executable(executable_path: &Path) -> Result { - // Return the path of the current executable by default - // Example C:\Program Files\My App\ - let extract_path = executable_path - .parent() - .map(PathBuf::from) - .ok_or(Error::FailedToDetermineExtractPath)?; - - // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp - // We need to get /Applications/.app - // TODO(lemarier): Need a better way here - // Maybe we could search for <*.app> to get the right path - #[cfg(target_os = "macos")] - if extract_path - .display() - .to_string() - .contains("Contents/MacOS") - { - return extract_path - .parent() - .map(PathBuf::from) - .ok_or(Error::FailedToDetermineExtractPath)? - .parent() - .map(PathBuf::from) - .ok_or(Error::FailedToDetermineExtractPath); - } - - Ok(extract_path) -} - -impl<'de> Deserialize<'de> for RemoteRelease { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct InnerRemoteRelease { - #[serde(alias = "name", deserialize_with = "parse_version")] - version: Version, - notes: Option, - pub_date: Option, - platforms: Option>, - // dynamic platform response - url: Option, - signature: Option, - } - - let release = InnerRemoteRelease::deserialize(deserializer)?; - - let pub_date = if let Some(date) = release.pub_date { - Some( - OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339) - .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?, - ) - } else { - None - }; - - Ok(RemoteRelease { - version: release.version, - notes: release.notes, - pub_date, - data: if let Some(platforms) = release.platforms { - RemoteReleaseInner::Static { platforms } - } else { - RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { - url: release.url.ok_or_else(|| { - DeError::custom("the `url` field was not set on the updater response") - })?, - signature: release.signature.ok_or_else(|| { - DeError::custom("the `signature` field was not set on the updater response") - })?, - }) - }, - }) - } -} - -fn parse_version<'de, D>(deserializer: D) -> std::result::Result -where - D: serde::Deserializer<'de>, -{ - let str = String::deserialize(deserializer)?; - - Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom) -} - -// Validate signature -fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result { - // we need to convert the pub key - let pub_key_decoded = base64_to_string(pub_key)?; - let public_key = PublicKey::decode(&pub_key_decoded)?; - let signature_base64_decoded = base64_to_string(release_signature)?; - let signature = Signature::decode(&signature_base64_decoded)?; - - // Validate signature or bail out - public_key.verify(data, &signature, true)?; - Ok(true) -} - -fn base64_to_string(base64_string: &str) -> Result { - let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?; - let result = std::str::from_utf8(decoded_string) - .map_err(|_| Error::SignatureUtf8(base64_string.into()))? - .to_string(); - Ok(result) -} - -#[cfg(windows)] -fn encode_wide(string: impl AsRef) -> Vec { - use std::os::windows::ffi::OsStrExt; - - string - .as_ref() - .encode_wide() - .chain(std::iter::once(0)) - .collect() -} - -#[cfg(windows)] -trait PathExt { - fn wrap_in_quotes(&self) -> Self; -} - -#[cfg(windows)] -impl PathExt for PathBuf { - fn wrap_in_quotes(&self) -> Self { - let mut msi_path = OsString::from("\""); - msi_path.push(self.as_os_str()); - msi_path.push("\""); - PathBuf::from(msi_path) - } -} - -#[cfg(windows)] -fn escape_msi_property_arg(arg: impl AsRef) -> String { - let mut arg = arg.as_ref().to_string_lossy().to_string(); - - // Otherwise this argument will get lost in ShellExecute - if arg.is_empty() { - return "\"\"\"\"".to_string(); - } else if !arg.contains(' ') && !arg.contains('"') { - return arg; - } - - if arg.contains('"') { - arg = arg.replace('"', r#""""""#) - } - - if arg.starts_with('-') { - if let Some((a1, a2)) = arg.split_once('=') { - format!("{a1}=\"\"{a2}\"\"") - } else { - format!("\"\"{arg}\"\"") - } - } else { - format!("\"\"{arg}\"\"") - } -} - -#[cfg(test)] -mod tests { - - #[test] - #[cfg(windows)] - fn it_wraps_correctly() { - use super::PathExt; - use std::path::PathBuf; - - assert_eq!( - PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(), - PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"") - ) - } - - #[test] - #[cfg(windows)] - fn it_escapes_correctly() { - use crate::updater::escape_msi_property_arg; - - // Explanation for quotes: - // The output of escape_msi_property_args() will be used in `LAUNCHAPPARGS=\"{HERE}\"`. This is the first quote level. - // To escape a quotation mark we use a second quotation mark, so "" is interpreted as " later. - // This means that the escaped strings can't ever have a single quotation mark! - // Now there are 3 major things to look out for to not break the msiexec call: - // 1) Wrap spaces in quotation marks, otherwise it will be interpreted as the end of the msiexec argument. - // 2) Escape escaping quotation marks, otherwise they will either end the msiexec argument or be ignored. - // 3) Escape emtpy args in quotation marks, otherwise the argument will get lost. - let cases = [ - "something", - "--flag", - "--empty=", - "--arg=value", - "some space", // This simulates `./my-app "some string"`. - "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic. - "--arg=unwrapped space", // `./my-app --arg="unwrapped space"` - "--arg=\"wrapped\"", // `./my-app --args=""wrapped""` - "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""` - "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""` - "", // `./my-app '""'` - ]; - let cases_escaped = [ - "something", - "--flag", - "--empty=", - "--arg=value", - "\"\"some space\"\"", - "\"\"--arg value\"\"", - "--arg=\"\"unwrapped space\"\"", - r#"--arg=""""""wrapped"""""""#, - r#"--arg=""""""wrapped space"""""""#, - r#"--arg=""midword""""wrapped space"""""""#, - "\"\"\"\"", - ]; - - // Just to be sure we didn't mess that up - assert_eq!(cases.len(), cases_escaped.len()); - - for (orig, escaped) in cases.iter().zip(cases_escaped) { - assert_eq!(escape_msi_property_arg(orig), escaped); - } - } -} +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + ffi::{OsStr, OsString}, + io::Cursor, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::Duration, +}; + +use base64::Engine; +use futures_util::StreamExt; +use http::HeaderName; +use minisign_verify::{PublicKey, Signature}; +use percent_encoding::{AsciiSet, CONTROLS}; +use reqwest::{ + header::{HeaderMap, HeaderValue}, + ClientBuilder, StatusCode, +}; +use semver::Version; +use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; +use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime}; +use time::OffsetDateTime; +use url::Url; + +use crate::{ + error::{Error, Result}, + Config, +}; + +const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +#[derive(Clone)] +pub enum Installer { + AppImage, + Deb, + Rpm, + + App, + + Msi, + Nsis, +} + +impl Installer{ + fn suffix(self) -> &'static str { + match self { + Self::AppImage => "appimage", + Self::Deb => "deb", + Self::Rpm => "rpm", + Self::App => "app", + Self::Msi => "msi", + Self::Nsis => "nsis", + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ReleaseManifestPlatform { + /// Download URL for the platform + pub url: Url, + /// Signature for the platform + pub signature: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum RemoteReleaseInner { + Dynamic(ReleaseManifestPlatform), + Static { + platforms: HashMap, + }, +} + +/// Information about a release returned by the remote update server. +/// +/// This type can have one of two shapes: Server Format (Dynamic Format) and Static Format. +#[derive(Debug, Clone)] +pub struct RemoteRelease { + /// Version to install. + pub version: Version, + /// Release notes. + pub notes: Option, + /// Release date. + pub pub_date: Option, + /// Release data. + pub data: RemoteReleaseInner, +} + +impl RemoteRelease { + /// The release's download URL for the given target. + pub fn download_url(&self, target: &str, installer: Option) -> Result<&Url> { + + let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); + match self.data { + RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url), + RemoteReleaseInner::Static { ref platforms } => platforms + .get(target) + .map_or_else( + || match fallback_target { + Some(fallback) => platforms.get(&fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback)), |p| Ok(&p.url)), + None => Err(Error::TargetNotFound(target.to_string())) + }, |p| { Ok(&p.url) }) + } + } + + /// The release's signature for the given target. + pub fn signature(&self, target: &str, installer: Option) -> Result<&String> { + let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); + + match self.data { + RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature), + RemoteReleaseInner::Static { ref platforms } => platforms + .get(target) + .map_or_else( + || match fallback_target { + Some(fallback) => platforms.get(&fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback)), |p| Ok(&p.signature)), + None => Err(Error::TargetNotFound(target.to_string())) + }, |p| { Ok(&p.signature) }) + } + } +} + +pub type OnBeforeExit = Arc; +pub type VersionComparator = Arc bool + Send + Sync>; +type MainThreadClosure = Box; +type RunOnMainThread = + Box std::result::Result<(), tauri::Error> + Send + Sync + 'static>; + +pub struct UpdaterBuilder { + #[allow(dead_code)] + run_on_main_thread: RunOnMainThread, + app_name: String, + current_version: Version, + config: Config, + pub(crate) version_comparator: Option, + executable_path: Option, + target: Option, + endpoints: Option>, + headers: HeaderMap, + timeout: Option, + proxy: Option, + installer_args: Vec, + current_exe_args: Vec, + on_before_exit: Option, +} + +impl UpdaterBuilder { + pub(crate) fn new(app: &AppHandle, config: crate::Config) -> Self { + let app_ = app.clone(); + let run_on_main_thread = + move |f: Box| app_.run_on_main_thread(f); + Self { + run_on_main_thread: Box::new(run_on_main_thread), + installer_args: config + .windows + .as_ref() + .map(|w| w.installer_args.clone()) + .unwrap_or_default(), + current_exe_args: Vec::new(), + app_name: app.package_info().name.clone(), + current_version: app.package_info().version.clone(), + config, + version_comparator: None, + executable_path: None, + target: None, + endpoints: None, + headers: Default::default(), + timeout: None, + proxy: None, + on_before_exit: None, + } + } + + pub fn version_comparator bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.version_comparator = Some(Arc::new(f)); + self + } + + pub fn target(mut self, target: impl Into) -> Self { + self.target.replace(target.into()); + self + } + + pub fn endpoints(mut self, endpoints: Vec) -> Result { + crate::config::validate_endpoints( + &endpoints, + self.config.dangerous_insecure_transport_protocol, + )?; + + self.endpoints.replace(endpoints); + Ok(self) + } + + pub fn executable_path>(mut self, p: P) -> Self { + self.executable_path.replace(p.as_ref().into()); + self + } + + pub fn header(mut self, key: K, value: V) -> Result + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + let key: std::result::Result = key.try_into().map_err(Into::into); + let value: std::result::Result = + value.try_into().map_err(Into::into); + self.headers.insert(key?, value?); + + Ok(self) + } + + pub fn headers(mut self, headers: HeaderMap) -> Self { + self.headers = headers; + self + } + + pub fn clear_headers(mut self) -> Self { + self.headers.clear(); + self + } + + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + pub fn proxy(mut self, proxy: Url) -> Self { + self.proxy.replace(proxy); + self + } + + pub fn pubkey>(mut self, pubkey: S) -> Self { + self.config.pubkey = pubkey.into(); + self + } + + pub fn installer_arg(mut self, arg: S) -> Self + where + S: Into, + { + self.installer_args.push(arg.into()); + self + } + + pub fn installer_args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + let args = args.into_iter().map(|a| a.into()).collect::>(); + self.installer_args.extend_from_slice(&args); + self + } + + pub fn clear_installer_args(mut self) -> Self { + self.installer_args.clear(); + self + } + + pub fn on_before_exit(mut self, f: F) -> Self { + self.on_before_exit.replace(Arc::new(f)); + self + } + + pub fn build(self) -> Result { + let endpoints = self + .endpoints + .unwrap_or_else(|| self.config.endpoints.clone()); + + if endpoints.is_empty() { + return Err(Error::EmptyEndpoints); + }; + + let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?; + let (target, json_target) = if let Some(target) = self.target { + (target.clone(), target ) + } else { + let target = get_updater_target().ok_or(Error::UnsupportedOs)?; + let json_target = format!("{target}-{arch}"); + (target.to_owned(), json_target) + }; + + let executable_path = self.executable_path.clone().unwrap_or(current_exe()?); + + // Get the extract_path from the provided executable_path + let extract_path = if cfg!(target_os = "linux") { + executable_path + } else { + extract_path_from_executable(&executable_path)? + }; + + Ok(Updater { + run_on_main_thread: Arc::new(self.run_on_main_thread), + config: self.config, + app_name: self.app_name, + current_version: self.current_version, + version_comparator: self.version_comparator, + timeout: self.timeout, + proxy: self.proxy, + endpoints, + installer_args: self.installer_args, + current_exe_args: self.current_exe_args, + arch, + target, + json_target, + headers: self.headers, + extract_path, + on_before_exit: self.on_before_exit, + }) + } +} + +impl UpdaterBuilder { + pub(crate) fn current_exe_args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + let args = args.into_iter().map(|a| a.into()).collect::>(); + self.current_exe_args.extend_from_slice(&args); + self + } +} + +pub struct Updater { + #[allow(dead_code)] + run_on_main_thread: Arc, + config: Config, + app_name: String, + current_version: Version, + version_comparator: Option, + timeout: Option, + proxy: Option, + endpoints: Vec, + arch: &'static str, + // The `{{target}}` variable we replace in the endpoint and serach for in the JSON + target: String, + // The value we search if the updater server returns a JSON with the `platforms` object + json_target: String, + headers: HeaderMap, + extract_path: PathBuf, + on_before_exit: Option, + #[allow(unused)] + installer_args: Vec, + #[allow(unused)] + current_exe_args: Vec, +} + + + + +impl Updater { + + fn get_updater_installer(&self) -> Result> { + match tauri::__TAURI_BUNDLE_TYPE { + "DEB_BUNDLE" => Ok(Some(Installer::Deb)), + "RPM_BUNDLE" => Ok(Some(Installer::Rpm)), + "APP_BUNDLE" => Ok(Some(Installer::AppImage)), + "MSI_BUNDLE" => Ok(Some(Installer::Msi)), + "NSS_BUNDLE" => Ok(Some(Installer::Nsis)), + _ => Err(Error::UnknownInstaller) + } + } + + pub async fn check(&self) -> Result> { + // we want JSON only + let mut headers = self.headers.clone(); + headers.insert("Accept", HeaderValue::from_str("application/json").unwrap()); + // Set SSL certs for linux if they aren't available. + #[cfg(target_os = "linux")] + { + if std::env::var_os("SSL_CERT_FILE").is_none() { + std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); + } + if std::env::var_os("SSL_CERT_DIR").is_none() { + std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); + } + } + + let mut remote_release: Option = None; + let mut raw_json: Option = None; + let mut last_error: Option = None; + for url in &self.endpoints { + // replace {{current_version}}, {{target}} and {{arch}} in the provided URL + // this is useful if we need to query example + // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}} + // will be translated into -> + // https://releases.myapp.com/update/darwin/aarch64/1.0.0 + // The main objective is if the update URL is defined via the Cargo.toml + // the URL will be generated dynamically + let version = self.current_version.to_string(); + let version = version.as_bytes(); + const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+'); + let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD); + let encoded_version = encoded_version.to_string(); + + let url: Url = url + .to_string() + // url::Url automatically url-encodes the path components + .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version) + .replace("%7B%7Btarget%7D%7D", &self.target) + .replace("%7B%7Barch%7D%7D", self.arch) + // but not query parameters + .replace("{{current_version}}", &encoded_version) + .replace("{{target}}", &self.target) + .replace("{{arch}}", self.arch) + .parse()?; + + let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); + if let Some(timeout) = self.timeout { + request = request.timeout(timeout); + } + if let Some(ref proxy) = self.proxy { + let proxy = reqwest::Proxy::all(proxy.as_str())?; + request = request.proxy(proxy); + } + let response = request + .build()? + .get(url) + .headers(headers.clone()) + .send() + .await; + + match response { + Ok(res) => { + if res.status().is_success() { + // no updates found! + if StatusCode::NO_CONTENT == res.status() { + return Ok(None); + }; + + raw_json = Some(res.json().await?); + match serde_json::from_value::(raw_json.clone().unwrap()) + .map_err(Into::into) + { + Ok(release) => { + last_error = None; + remote_release = Some(release); + // we found a relase, break the loop + break; + } + Err(err) => last_error = Some(err), + } + } + } + Err(err) => last_error = Some(err.into()), + } + } + + // Last error is cleaned on success. + // Shouldn't be triggered if we had a successfull call + if let Some(error) = last_error { + return Err(error); + } + + // Extracted remote metadata + let release = remote_release.ok_or(Error::ReleaseNotFound)?; + + let should_update = match self.version_comparator.as_ref() { + Some(comparator) => comparator(self.current_version.clone(), release.clone()), + None => release.version > self.current_version, + }; + + let installer = self.get_updater_installer()?; + + let update = if should_update { + Some(Update { + run_on_main_thread: self.run_on_main_thread.clone(), + config: self.config.clone(), + on_before_exit: self.on_before_exit.clone(), + app_name: self.app_name.clone(), + current_version: self.current_version.to_string(), + target: self.target.clone(), + extract_path: self.extract_path.clone(), + version: release.version.to_string(), + date: release.pub_date, + download_url: release.download_url(&self.json_target, installer.clone())?.to_owned(), + body: release.notes.clone(), + signature: release.signature(&self.json_target, installer.clone())?.to_owned(), + installer, + raw_json: raw_json.unwrap(), + timeout: self.timeout, + proxy: self.proxy.clone(), + headers: self.headers.clone(), + installer_args: self.installer_args.clone(), + current_exe_args: self.current_exe_args.clone(), + }) + } else { + None + }; + + Ok(update) + } +} + +#[derive(Clone)] +pub struct Update { + #[allow(dead_code)] + run_on_main_thread: Arc, + config: Config, + #[allow(unused)] + on_before_exit: Option, + /// Update description + pub body: Option, + /// Version used to check for update + pub current_version: String, + /// Version announced + pub version: String, + /// Update publish date + pub date: Option, + /// Target + pub target: String, + /// Current installer + pub installer: Option, + /// Download URL announced + pub download_url: Url, + /// Signature announced + pub signature: String, + /// The raw version of server's JSON response. Useful if the response contains additional fields that the updater doesn't handle. + pub raw_json: serde_json::Value, + /// Request timeout + pub timeout: Option, + /// Request proxy + pub proxy: Option, + /// Request headers + pub headers: HeaderMap, + /// Extract path + #[allow(unused)] + extract_path: PathBuf, + /// App name, used for creating named tempfiles on Windows + #[allow(unused)] + app_name: String, + #[allow(unused)] + installer_args: Vec, + #[allow(unused)] + current_exe_args: Vec, +} + +impl Resource for Update {} + +impl Update { + /// Downloads the updater package, verifies it then return it as bytes. + /// + /// Use [`Update::install`] to install it + pub async fn download), D: FnOnce()>( + &self, + mut on_chunk: C, + on_download_finish: D, + ) -> Result> { + // set our headers + let mut headers = self.headers.clone(); + headers.insert( + "Accept", + HeaderValue::from_str("application/octet-stream").unwrap(), + ); + + let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); + if let Some(timeout) = self.timeout { + request = request.timeout(timeout); + } + if let Some(ref proxy) = self.proxy { + let proxy = reqwest::Proxy::all(proxy.as_str())?; + request = request.proxy(proxy); + } + let response = request + .build()? + .get(self.download_url.clone()) + .headers(headers) + .send() + .await?; + + if !response.status().is_success() { + return Err(Error::Network(format!( + "Download request failed with status: {}", + response.status() + ))); + } + + let content_length: Option = response + .headers() + .get("Content-Length") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse().ok()); + + let mut buffer = Vec::new(); + + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + on_chunk(chunk.len(), content_length); + buffer.extend(chunk); + } + on_download_finish(); + + verify_signature(&buffer, &self.signature, &self.config.pubkey)?; + + Ok(buffer) + } + + /// Installs the updater package downloaded by [`Update::download`] + pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> { + self.install_inner(bytes.as_ref()) + } + + /// Downloads and installs the updater package + pub async fn download_and_install), D: FnOnce()>( + &self, + on_chunk: C, + on_download_finish: D, + ) -> Result<()> { + let bytes = self.download(on_chunk, on_download_finish).await?; + self.install(bytes) + } + + #[cfg(mobile)] + fn install_inner(&self, _bytes: &[u8]) -> Result<()> { + Ok(()) + } +} + +#[cfg(windows)] +enum WindowsUpdaterType { + Nsis { + path: PathBuf, + #[allow(unused)] + temp: Option, + }, + Msi { + path: PathBuf, + #[allow(unused)] + temp: Option, + }, +} + +#[cfg(windows)] +impl WindowsUpdaterType { + fn nsis(path: PathBuf, temp: Option) -> Self { + Self::Nsis { path, temp } + } + + fn msi(path: PathBuf, temp: Option) -> Self { + Self::Msi { + path: path.wrap_in_quotes(), + temp, + } + } +} + +#[cfg(windows)] +impl Config { + fn install_mode(&self) -> crate::config::WindowsUpdateInstallMode { + self.windows + .as_ref() + .map(|w| w.install_mode.clone()) + .unwrap_or_default() + } +} + +/// Windows +#[cfg(windows)] +impl Update { + /// ### Expected structure: + /// ├── [AppName]_[version]_x64.msi # Application MSI + /// ├── [AppName]_[version]_x64-setup.exe # NSIS installer + /// ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler + /// │ └──[AppName]_[version]_x64.msi # Application MSI + /// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler + /// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer + /// └── ... + fn install_inner(&self, bytes: &[u8]) -> Result<()> { + use std::iter::once; + use windows_sys::{ + w, + Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW}, + }; + + let updater_type = self.extract(bytes)?; + + let install_mode = self.config.install_mode(); + let current_args = &self.current_exe_args()[1..]; + let msi_args; + + let installer_args: Vec<&OsStr> = match &updater_type { + WindowsUpdaterType::Nsis { .. } => install_mode + .nsis_args() + .iter() + .map(OsStr::new) + .chain(once(OsStr::new("/UPDATE"))) + .chain(once(OsStr::new("/ARGS"))) + .chain(current_args.to_vec()) + .chain(self.installer_args()) + .collect(), + WindowsUpdaterType::Msi { path, .. } => { + let escaped_args = current_args + .iter() + .map(escape_msi_property_arg) + .collect::>() + .join(" "); + msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\"")); + + [OsStr::new("/i"), path.as_os_str()] + .into_iter() + .chain(install_mode.msiexec_args().iter().map(OsStr::new)) + .chain(once(OsStr::new("/promptrestart"))) + .chain(self.installer_args()) + .chain(once(OsStr::new("AUTOLAUNCHAPP=True"))) + .chain(once(msi_args.as_os_str())) + .collect() + } + }; + + if let Some(on_before_exit) = self.on_before_exit.as_ref() { + on_before_exit(); + } + + let file = match &updater_type { + WindowsUpdaterType::Nsis { path, .. } => path.as_os_str().to_os_string(), + WindowsUpdaterType::Msi { .. } => std::env::var("SYSTEMROOT").as_ref().map_or_else( + |_| OsString::from("msiexec.exe"), + |p| OsString::from(format!("{p}\\System32\\msiexec.exe")), + ), + }; + let file = encode_wide(file); + + let parameters = installer_args.join(OsStr::new(" ")); + let parameters = encode_wide(parameters); + + unsafe { + ShellExecuteW( + std::ptr::null_mut(), + w!("open"), + file.as_ptr(), + parameters.as_ptr(), + std::ptr::null(), + SW_SHOW, + ) + }; + + std::process::exit(0); + } + + fn installer_args(&self) -> Vec<&OsStr> { + self.installer_args + .iter() + .map(OsStr::new) + .collect::>() + } + + fn current_exe_args(&self) -> Vec<&OsStr> { + self.current_exe_args + .iter() + .map(OsStr::new) + .collect::>() + } + + fn extract(&self, bytes: &[u8]) -> Result { + #[cfg(feature = "zip")] + if infer::archive::is_zip(bytes) { + return self.extract_zip(bytes); + } + + self.extract_exe(bytes) + } + + fn make_temp_dir(&self) -> Result { + Ok(tempfile::Builder::new() + .prefix(&format!("{}-{}-updater-", self.app_name, self.version)) + .tempdir()? + .into_path()) + } + + #[cfg(feature = "zip")] + fn extract_zip(&self, bytes: &[u8]) -> Result { + let temp_dir = self.make_temp_dir()?; + + let archive = Cursor::new(bytes); + let mut extractor = zip::ZipArchive::new(archive)?; + extractor.extract(&temp_dir)?; + + let paths = std::fs::read_dir(&temp_dir)?; + for path in paths { + let path = path?.path(); + let ext = path.extension(); + if ext == Some(OsStr::new("exe")) { + return Ok(WindowsUpdaterType::nsis(path, None)); + } else if ext == Some(OsStr::new("msi")) { + return Ok(WindowsUpdaterType::msi(path, None)); + } + } + + Err(crate::Error::BinaryNotFoundInArchive) + } + + fn extract_exe(&self, bytes: &[u8]) -> Result { + if infer::app::is_exe(bytes) { + let (path, temp) = self.write_to_temp(bytes, ".exe")?; + Ok(WindowsUpdaterType::nsis(path, temp)) + } else if infer::archive::is_msi(bytes) { + let (path, temp) = self.write_to_temp(bytes, ".msi")?; + Ok(WindowsUpdaterType::msi(path, temp)) + } else { + Err(crate::Error::InvalidUpdaterFormat) + } + } + + fn write_to_temp( + &self, + bytes: &[u8], + ext: &str, + ) -> Result<(PathBuf, Option)> { + use std::io::Write; + + let temp_dir = self.make_temp_dir()?; + let mut temp_file = tempfile::Builder::new() + .prefix(&format!("{}-{}-installer", self.app_name, self.version)) + .suffix(ext) + .rand_bytes(0) + .tempfile_in(temp_dir)?; + temp_file.write_all(bytes)?; + + let temp = temp_file.into_temp_path(); + Ok((temp.to_path_buf(), Some(temp))) + } +} + +/// Linux (AppImage and Deb) +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +impl Update { + /// ### Expected structure: + /// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler + /// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage + /// ├── [AppName]_[version]_amd64.deb # Debian package + /// └── ... + /// + fn install_inner(&self, bytes: &[u8]) -> Result<()> { + match self.installer { + Some(Installer::Deb) => self.install_deb(bytes), + Some(Installer::Rpm) => self.install_rpm(bytes), + _ =>self.install_appimage(bytes) + } + } + + fn install_appimage(&self, bytes: &[u8]) -> Result<()> { + use std::os::unix::fs::{MetadataExt, PermissionsExt}; + let extract_path_metadata = self.extract_path.metadata()?; + + let tmp_dir_locations = vec![ + Box::new(|| Some(std::env::temp_dir())) as Box Option>, + Box::new(dirs::cache_dir), + Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), + ]; + + for tmp_dir_location in tmp_dir_locations { + if let Some(tmp_dir_location) = tmp_dir_location() { + let tmp_dir = tempfile::Builder::new() + .prefix("tauri_current_app") + .tempdir_in(tmp_dir_location)?; + let tmp_dir_metadata = tmp_dir.path().metadata()?; + + if extract_path_metadata.dev() == tmp_dir_metadata.dev() { + let mut perms = tmp_dir_metadata.permissions(); + perms.set_mode(0o700); + std::fs::set_permissions(tmp_dir.path(), perms)?; + + let tmp_app_image = &tmp_dir.path().join("current_app.AppImage"); + + let permissions = std::fs::metadata(&self.extract_path)?.permissions(); + + // create a backup of our current app image + std::fs::rename(&self.extract_path, tmp_app_image)?; + + #[cfg(feature = "zip")] + if infer::archive::is_gz(bytes) { + // extract the buffer to the tmp_dir + // we extract our signed archive into our final directory without any temp file + let archive = Cursor::new(bytes); + let decoder = flate2::read::GzDecoder::new(archive); + let mut archive = tar::Archive::new(decoder); + for mut entry in archive.entries()?.flatten() { + if let Ok(path) = entry.path() { + if path.extension() == Some(OsStr::new("AppImage")) { + // if something went wrong during the extraction, we should restore previous app + if let Err(err) = entry.unpack(&self.extract_path) { + std::fs::rename(tmp_app_image, &self.extract_path)?; + return Err(err.into()); + } + // early finish we have everything we need here + return Ok(()); + } + } + } + // if we have not returned early we should restore the backup + std::fs::rename(tmp_app_image, &self.extract_path)?; + return Err(Error::BinaryNotFoundInArchive); + } + + return match std::fs::write(&self.extract_path, bytes) + .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions)) + { + Err(err) => { + // if something went wrong during the extraction, we should restore previous app + std::fs::rename(tmp_app_image, &self.extract_path)?; + Err(err.into()) + } + Ok(_) => Ok(()), + }; + } + } + } + + Err(Error::TempDirNotOnSameMountPoint) + } + + + fn install_deb(&self, bytes: &[u8]) -> Result<()> { + // First verify the bytes are actually a .deb package + if !infer::archive::is_deb(bytes) { + return Err(Error::InvalidUpdaterFormat); + } + + self.try_tmp_locations(bytes, "dpkg", "-i") + + } + + fn install_rpm(&self, bytes: &[u8]) -> Result<()> { + // First verify the bytes are actually a .rpm package + if !infer::archive::is_rpm(bytes) { + return Err(Error::InvalidUpdaterFormat); + } + self.try_tmp_locations(bytes, "rpm", "-U") + } + + fn try_tmp_locations(&self, bytes: &[u8], install_cmd: &str, install_arg: &str) -> Result<()> { + // Try different temp directories + let tmp_dir_locations = vec![ + Box::new(|| Some(std::env::temp_dir())) as Box Option>, + Box::new(dirs::cache_dir), + Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), + ]; + + // Try writing to multiple temp locations until one succeeds + for tmp_dir_location in tmp_dir_locations { + if let Some(path) = tmp_dir_location() { + if let Ok(tmp_dir) = tempfile::Builder::new() + .prefix("tauri_rpm_update") + .tempdir_in(path) + { + let pkg_path = tmp_dir.path().join("package.rpm"); + + // Try writing the .deb file + if std::fs::write(&pkg_path, bytes).is_ok() { + // If write succeeds, proceed with installation + return self.try_install_with_privileges(&pkg_path, install_cmd, install_arg); + } + // If write fails, continue to next temp location + } + } + } + + // If we get here, all temp locations failed + Err(Error::TempDirNotFound) + + } + + fn try_install_with_privileges(&self, pkg_path: &Path, install_cmd: &str, install_arg: &str) -> Result<()> { + // 1. First try using pkexec (graphical sudo prompt) + if let Ok(status) = std::process::Command::new("pkexec") + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) + .status() + { + if status.success() { + return Ok(()); + } + } + + // 2. Try zenity or kdialog for a graphical sudo experience + if let Ok(password) = self.get_password_graphically() { + if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? { + return Ok(()); + } + } + + // 3. Final fallback: terminal sudo + let status = std::process::Command::new("sudo") + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) + .status()?; + + if status.success() { + Ok(()) + } else { + Err(Error::PackageInstallFailed) + } + } + + fn get_password_graphically(&self) -> Result { + // Try zenity first + let zenity_result = std::process::Command::new("zenity") + .args([ + "--password", + "--title=Authentication Required", + "--text=Enter your password to install the update:", + ]) + .output(); + + if let Ok(output) = zenity_result { + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + } + + // Fall back to kdialog if zenity fails or isn't available + let kdialog_result = std::process::Command::new("kdialog") + .args(["--password", "Enter your password to install the update:"]) + .output(); + + if let Ok(output) = kdialog_result { + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + } + + Err(Error::AuthenticationFailed) + } + + fn install_with_sudo(&self, pkg_path: &Path, password: &str, install_cmd: &str, install_arg: &str) -> Result { + use std::io::Write; + use std::process::{Command, Stdio}; + + let mut child = Command::new("sudo") + .arg("-S") // read password from stdin + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + if let Some(mut stdin) = child.stdin.take() { + // Write password to stdin + writeln!(stdin, "{}", password)?; + } + + let status = child.wait()?; + Ok(status.success()) + } +} + +/// MacOS +#[cfg(target_os = "macos")] +impl Update { + /// ### Expected structure: + /// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler + /// │ └──[AppName].app # Main application + /// │ └── Contents # Application contents... + /// │ └── ... + /// └── ... + fn install_inner(&self, bytes: &[u8]) -> Result<()> { + use flate2::read::GzDecoder; + + let cursor = Cursor::new(bytes); + let mut extracted_files: Vec = Vec::new(); + + // Create temp directories for backup and extraction + let tmp_backup_dir = tempfile::Builder::new() + .prefix("tauri_current_app") + .tempdir()?; + + let tmp_extract_dir = tempfile::Builder::new() + .prefix("tauri_updated_app") + .tempdir()?; + + let decoder = GzDecoder::new(cursor); + let mut archive = tar::Archive::new(decoder); + + // Extract files to temporary directory + for entry in archive.entries()? { + let mut entry = entry?; + let collected_path: PathBuf = entry.path()?.iter().skip(1).collect(); + let extraction_path = tmp_extract_dir.path().join(&collected_path); + + // Ensure parent directories exist + if let Some(parent) = extraction_path.parent() { + std::fs::create_dir_all(parent)?; + } + + if let Err(err) = entry.unpack(&extraction_path) { + // Cleanup on error + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(err.into()); + } + extracted_files.push(extraction_path); + } + + // Try to move the current app to backup + let move_result = std::fs::rename( + &self.extract_path, + tmp_backup_dir.path().join("current_app"), + ); + let need_authorization = if let Err(err) = move_result { + if err.kind() == std::io::ErrorKind::PermissionDenied { + true + } else { + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(err.into()); + } + } else { + false + }; + + if need_authorization { + // Use AppleScript to perform moves with admin privileges + let apple_script = format!( + "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges", + src = self.extract_path.display(), + new = tmp_extract_dir.path().display() + ); + + let (tx, rx) = std::sync::mpsc::channel(); + let res = (self.run_on_main_thread)(Box::new(move || { + let mut script = + osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script); + script.compile().expect("invalid AppleScript"); + let r = script.execute(); + tx.send(r).unwrap(); + })); + let result = rx.recv().unwrap(); + + if res.is_err() || result.is_err() { + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Failed to move the new app into place", + ))); + } + } else { + // Remove existing directory if it exists + if self.extract_path.exists() { + std::fs::remove_dir_all(&self.extract_path)?; + } + // Move the new app to the target path + std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?; + } + + let _ = std::process::Command::new("touch") + .arg(&self.extract_path) + .status(); + + Ok(()) + } +} + +/// Gets the target string used on the updater. +pub fn target() -> Option { + if let (Some(target), Some(arch)) = (get_updater_target(), get_updater_arch()) { + Some(format!("{target}-{arch}")) + } else { + None + } +} + +pub(crate) fn get_updater_target() -> Option<&'static str> { + if cfg!(target_os = "linux") { + Some("linux") + } else if cfg!(target_os = "macos") { + // TODO shouldn't this be macos instead? + Some("darwin") + } else if cfg!(target_os = "windows") { + Some("windows") + } else { + None + } +} + + +pub(crate) fn get_updater_arch() -> Option<&'static str> { + if cfg!(target_arch = "x86") { + Some("i686") + } else if cfg!(target_arch = "x86_64") { + Some("x86_64") + } else if cfg!(target_arch = "arm") { + Some("armv7") + } else if cfg!(target_arch = "aarch64") { + Some("aarch64") + } else { + None + } +} + + + +pub fn extract_path_from_executable(executable_path: &Path) -> Result { + // Return the path of the current executable by default + // Example C:\Program Files\My App\ + let extract_path = executable_path + .parent() + .map(PathBuf::from) + .ok_or(Error::FailedToDetermineExtractPath)?; + + // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp + // We need to get /Applications/.app + // TODO(lemarier): Need a better way here + // Maybe we could search for <*.app> to get the right path + #[cfg(target_os = "macos")] + if extract_path + .display() + .to_string() + .contains("Contents/MacOS") + { + return extract_path + .parent() + .map(PathBuf::from) + .ok_or(Error::FailedToDetermineExtractPath)? + .parent() + .map(PathBuf::from) + .ok_or(Error::FailedToDetermineExtractPath); + } + + Ok(extract_path) +} + +impl<'de> Deserialize<'de> for RemoteRelease { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct InnerRemoteRelease { + #[serde(alias = "name", deserialize_with = "parse_version")] + version: Version, + notes: Option, + pub_date: Option, + platforms: Option>, + // dynamic platform response + url: Option, + signature: Option, + } + + let release = InnerRemoteRelease::deserialize(deserializer)?; + + let pub_date = if let Some(date) = release.pub_date { + Some( + OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339) + .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?, + ) + } else { + None + }; + + Ok(RemoteRelease { + version: release.version, + notes: release.notes, + pub_date, + data: if let Some(platforms) = release.platforms { + RemoteReleaseInner::Static { platforms } + } else { + RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { + url: release.url.ok_or_else(|| { + DeError::custom("the `url` field was not set on the updater response") + })?, + signature: release.signature.ok_or_else(|| { + DeError::custom("the `signature` field was not set on the updater response") + })?, + }) + }, + }) + } +} + +fn parse_version<'de, D>(deserializer: D) -> std::result::Result +where + D: serde::Deserializer<'de>, +{ + let str = String::deserialize(deserializer)?; + + Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom) +} + +// Validate signature +fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result { + // we need to convert the pub key + let pub_key_decoded = base64_to_string(pub_key)?; + let public_key = PublicKey::decode(&pub_key_decoded)?; + let signature_base64_decoded = base64_to_string(release_signature)?; + let signature = Signature::decode(&signature_base64_decoded)?; + + // Validate signature or bail out + public_key.verify(data, &signature, true)?; + Ok(true) +} + +fn base64_to_string(base64_string: &str) -> Result { + let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?; + let result = std::str::from_utf8(decoded_string) + .map_err(|_| Error::SignatureUtf8(base64_string.into()))? + .to_string(); + Ok(result) +} + +#[cfg(windows)] +fn encode_wide(string: impl AsRef) -> Vec { + use std::os::windows::ffi::OsStrExt; + + string + .as_ref() + .encode_wide() + .chain(std::iter::once(0)) + .collect() +} + +#[cfg(windows)] +trait PathExt { + fn wrap_in_quotes(&self) -> Self; +} + +#[cfg(windows)] +impl PathExt for PathBuf { + fn wrap_in_quotes(&self) -> Self { + let mut msi_path = OsString::from("\""); + msi_path.push(self.as_os_str()); + msi_path.push("\""); + PathBuf::from(msi_path) + } +} + +#[cfg(windows)] +fn escape_msi_property_arg(arg: impl AsRef) -> String { + let mut arg = arg.as_ref().to_string_lossy().to_string(); + + // Otherwise this argument will get lost in ShellExecute + if arg.is_empty() { + return "\"\"\"\"".to_string(); + } else if !arg.contains(' ') && !arg.contains('"') { + return arg; + } + + if arg.contains('"') { + arg = arg.replace('"', r#""""""#) + } + + if arg.starts_with('-') { + if let Some((a1, a2)) = arg.split_once('=') { + format!("{a1}=\"\"{a2}\"\"") + } else { + format!("\"\"{arg}\"\"") + } + } else { + format!("\"\"{arg}\"\"") + } +} + +#[cfg(test)] +mod tests { + + #[test] + #[cfg(windows)] + fn it_wraps_correctly() { + use super::PathExt; + use std::path::PathBuf; + + assert_eq!( + PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(), + PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"") + ) + } + + #[test] + #[cfg(windows)] + fn it_escapes_correctly() { + use crate::updater::escape_msi_property_arg; + + // Explanation for quotes: + // The output of escape_msi_property_args() will be used in `LAUNCHAPPARGS=\"{HERE}\"`. This is the first quote level. + // To escape a quotation mark we use a second quotation mark, so "" is interpreted as " later. + // This means that the escaped strings can't ever have a single quotation mark! + // Now there are 3 major things to look out for to not break the msiexec call: + // 1) Wrap spaces in quotation marks, otherwise it will be interpreted as the end of the msiexec argument. + // 2) Escape escaping quotation marks, otherwise they will either end the msiexec argument or be ignored. + // 3) Escape emtpy args in quotation marks, otherwise the argument will get lost. + let cases = [ + "something", + "--flag", + "--empty=", + "--arg=value", + "some space", // This simulates `./my-app "some string"`. + "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic. + "--arg=unwrapped space", // `./my-app --arg="unwrapped space"` + "--arg=\"wrapped\"", // `./my-app --args=""wrapped""` + "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""` + "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""` + "", // `./my-app '""'` + ]; + let cases_escaped = [ + "something", + "--flag", + "--empty=", + "--arg=value", + "\"\"some space\"\"", + "\"\"--arg value\"\"", + "--arg=\"\"unwrapped space\"\"", + r#"--arg=""""""wrapped"""""""#, + r#"--arg=""""""wrapped space"""""""#, + r#"--arg=""midword""""wrapped space"""""""#, + "\"\"\"\"", + ]; + + // Just to be sure we didn't mess that up + assert_eq!(cases.len(), cases_escaped.len()); + + for (orig, escaped) in cases.iter().zip(cases_escaped) { + assert_eq!(escape_msi_property_arg(orig), escaped); + } + } +} diff --git a/plugins/updater/tests/app-updater/src/main.rs b/plugins/updater/tests/app-updater/src/main.rs index 5f5566f8..d3defc1e 100644 --- a/plugins/updater/tests/app-updater/src/main.rs +++ b/plugins/updater/tests/app-updater/src/main.rs @@ -1,56 +1,57 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use tauri_plugin_updater::UpdaterExt; - -fn main() { - #[allow(unused_mut)] - let mut context = tauri::generate_context!(); - tauri::Builder::default() - .plugin(tauri_plugin_updater::Builder::new().build()) - .setup(|app| { - let handle = app.handle().clone(); - tauri::async_runtime::spawn(async move { - let mut builder = handle.updater_builder(); - if std::env::var("TARGET").unwrap_or_default() == "nsis" { - // /D sets the default installation directory ($INSTDIR), - // overriding InstallDir and InstallDirRegKey. - // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. - // Only absolute paths are supported. - // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder - builder = builder.installer_args(vec![format!( - "/D={}", - tauri::utils::platform::current_exe() - .unwrap() - .parent() - .unwrap() - .display() - )]); - } - let updater = builder.build().unwrap(); - - match updater.check().await { - Ok(Some(update)) => { - if let Err(e) = update.download_and_install(|_, _| {}, || {}).await { - println!("{e}"); - std::process::exit(1); - } - std::process::exit(0); - } - Ok(None) => { - std::process::exit(2); - } - Err(e) => { - println!("{e}"); - std::process::exit(1); - } - } - }); - Ok(()) - }) - .run(context) - .expect("error while running tauri application"); -} +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri_plugin_updater::UpdaterExt; + +fn main() { + #[allow(unused_mut)] + let mut context = tauri::generate_context!(); + println!("{}", tauri::__TAURI_BUNDLE_TYPE); + tauri::Builder::default() + .plugin(tauri_plugin_updater::Builder::new().build()) + .setup(|app| { + let handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let mut builder = handle.updater_builder(); + if std::env::var("TARGET").unwrap_or_default() == "nsis" { + // /D sets the default installation directory ($INSTDIR), + // overriding InstallDir and InstallDirRegKey. + // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. + // Only absolute paths are supported. + // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder + builder = builder.installer_args(vec![format!( + "/D={}", + tauri::utils::platform::current_exe() + .unwrap() + .parent() + .unwrap() + .display() + )]); + } + let updater = builder.build().unwrap(); + + match updater.check().await { + Ok(Some(update)) => { + if let Err(e) = update.download_and_install(|_, _| {}, || {}).await { + println!("{e}"); + std::process::exit(1); + } + std::process::exit(0); + } + Ok(None) => { + std::process::exit(2); + } + Err(e) => { + println!("{e}"); + std::process::exit(1); + } + } + }); + Ok(()) + }) + .run(context) + .expect("error while running tauri application"); +}