feat: Windows RAM profiles

This commit is contained in:
zhom committed 2026-10-19 05:38:20 +04:00
1 parent f493c9ef7c
commit 58b0febe42
29 files changed
+3087 -109

No files matched your search

+1
View File
@@ -76,6 +76,7 @@ donutbrowser/
│ │ ├── feedback.rs # Feedback to the Donut team from the dialog and the send_feedback MCP tool, optional redacted logs
│ │ ├── sync/ # Cloud sync (engine, encryption, manifest, scheduler)
│ │ ├── vpn/ # WireGuard tunnels
│ │ ├── ramfs/ # Windows RAM volume for ephemeral profiles: in-memory file system on WinFsp, hosted by donut-proxy
│ │ ├── xray/ # Xray share links (VLESS, VMess, Trojan, Hysteria2): parse, export, client config
│ │ ├── xray_worker_runner.rs # Xray-core sidecar lifecycle
│ │ ├── xray_worker_storage.rs # Private Xray worker state and runtime files
+1
View File
@@ -78,6 +78,7 @@ export const commandCoverage = {
"profile_totp::add_profile_totp",
"profile_totp::delete_profile_totp",
"profile_totp::get_profile_totp_codes",
"ephemeral_dirs::get_ephemeral_storage_status",
"rename_profile",
"detect_existing_profiles",
"import_browser_profiles",
+11
View File
@@ -1522,6 +1522,17 @@ test("a storage state file moves cookies and Local Storage into a closed profile
}),
/LOCAL_STORAGE_EPHEMERAL/,
);
const storage = await app.invoke("get_ephemeral_storage_status");
assert.deepEqual(Object.keys(storage).sort(), ["in_memory", "reason"]);
if (process.platform === "win32") {
assert.equal(storage.in_memory, storage.reason === null);
if (storage.reason !== null) {
assert.match(storage.reason, /^winfsp_(missing|error)$/);
}
} else {
assert.deepEqual(storage, { in_memory: true, reason: null });
}
});
});
+284 -65
View File
@@ -182,7 +182,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -193,7 +193,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -224,7 +224,7 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -470,7 +470,7 @@ dependencies = [
"smappservice-rs",
"thiserror 2.0.21",
"windows-registry",
"windows-result",
"windows-result 0.4.1",
]
[[package]]
@@ -508,7 +508,7 @@ dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"nom 8.0.0",
"num-rational",
"v_frame",
]
@@ -601,6 +601,26 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.13.2",
"cexpr",
"clang-sys",
"itertools 0.13.0",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex 1.3.0",
"syn 2.0.119",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -957,7 +977,7 @@ dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
"shlex 2.0.1",
]
[[package]]
@@ -966,6 +986,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "cfb"
version = "0.10.0"
@@ -1056,7 +1085,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -1091,6 +1120,17 @@ dependencies = [
"inout 0.2.4",
]
[[package]]
name = "clang-sys"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
dependencies = [
"glob",
"libc",
"libloading 0.8.9",
]
[[package]]
name = "clap"
version = "4.6.7"
@@ -1672,7 +1712,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -1837,7 +1877,9 @@ dependencies = [
"utoipa",
"utoipa-axum",
"uuid",
"windows",
"windows 0.62.2",
"winfsp",
"winfsp-sys",
"winreg",
"wiremock",
"yaml-rust2",
@@ -2071,7 +2113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2560,7 +2602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -3119,7 +3161,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
"windows-core 0.62.2",
]
[[package]]
@@ -3424,6 +3466,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -3476,7 +3527,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -3666,7 +3717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
dependencies = [
"gtk-sys",
"libloading",
"libloading 0.7.4",
"once_cell",
]
@@ -3711,6 +3762,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link 0.2.1",
]
[[package]]
name = "libm"
version = "0.2.16"
@@ -3907,6 +3968,12 @@ dependencies = [
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -3990,7 +4057,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.21",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4073,6 +4140,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
@@ -4625,7 +4702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.45.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4679,7 +4756,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -5009,6 +5086,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.119",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -5266,7 +5353,7 @@ dependencies = [
"built",
"cfg-if",
"interpolate_name",
"itertools",
"itertools 0.14.0",
"libc",
"libfuzzer-sys",
"log",
@@ -5621,7 +5708,7 @@ dependencies = [
"errno 0.3.14",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6161,6 +6248,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "shlex"
version = "2.0.1"
@@ -6289,7 +6382,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6377,6 +6470,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "static_vcruntime"
version = "3.0.0"
@@ -6522,7 +6621,7 @@ dependencies = [
"objc2-core-foundation",
"objc2-io-kit",
"objc2-open-directory",
"windows",
"windows 0.62.2",
]
[[package]]
@@ -6593,8 +6692,8 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows-core",
"windows 0.62.2",
"windows-core 0.62.2",
"windows-version",
"x11-dl",
]
@@ -6676,7 +6775,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.62.2",
]
[[package]]
@@ -6806,7 +6905,7 @@ dependencies = [
"tracing",
"url",
"windows-registry",
"windows-result",
"windows-result 0.4.1",
]
[[package]]
@@ -6925,7 +7024,7 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.21",
"url",
"windows",
"windows 0.62.2",
"zbus",
]
@@ -7005,7 +7104,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.62.2",
]
[[package]]
@@ -7030,7 +7129,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.62.2",
"wry",
]
@@ -7090,7 +7189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
dependencies = [
"thiserror 2.0.21",
"windows",
"windows 0.62.2",
"windows-version",
]
@@ -7101,10 +7200,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7590,7 +7689,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.21",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -7600,7 +7699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"nom 8.0.0",
"petgraph",
]
@@ -7669,7 +7768,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -8160,8 +8259,8 @@ checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows-core",
"windows 0.62.2",
"windows-core 0.62.2",
]
[[package]]
@@ -8182,8 +8281,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
dependencies = [
"thiserror 2.0.21",
"windows",
"windows-core",
"windows 0.62.2",
"windows-core 0.62.2",
]
[[package]]
@@ -8192,6 +8291,12 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -8214,7 +8319,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -8235,20 +8340,42 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
"windows-version",
]
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections 0.2.0",
"windows-core 0.61.2",
"windows-future 0.2.1",
"windows-link 0.1.3",
"windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.2",
]
[[package]]
@@ -8257,7 +8384,20 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
@@ -8268,9 +8408,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading 0.1.0",
]
[[package]]
@@ -8279,9 +8430,9 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
]
[[package]]
@@ -8306,20 +8457,36 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]]
@@ -8328,9 +8495,18 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
@@ -8339,7 +8515,16 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
@@ -8348,7 +8533,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -8384,7 +8569,7 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -8424,7 +8609,7 @@ version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
@@ -8435,13 +8620,22 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -8450,7 +8644,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -8591,6 +8785,31 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winfsp"
version = "0.13.1+winfsp-2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5132f201f1a350673ec9eb7d13be9162e16ea057a44043ef64635aa749b16c53"
dependencies = [
"bytemuck",
"parking_lot",
"paste",
"static_assertions",
"thiserror 2.0.21",
"widestring",
"windows 0.61.3",
"winfsp-sys",
]
[[package]]
name = "winfsp-sys"
version = "0.12.1+winfsp-2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d42acc30c105f8d33507f556398237c738b681271c775a3b94eefd29d2b8c77e"
dependencies = [
"bindgen",
]
[[package]]
name = "winnow"
version = "0.5.40"
@@ -8616,7 +8835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [
"cfg-if",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -8710,8 +8929,8 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows-core",
"windows 0.62.2",
"windows-core 0.62.2",
"windows-version",
"x11-dl",
]
+11
View File
@@ -182,7 +182,18 @@ windows = { version = "0.62", features = [
# CryptUnprotectData, for unwrapping the source browser's os_crypt key from
# `Local State` during profile import.
"Win32_Security_Cryptography",
# The RAM volume (src/ramfs): its root security descriptor, and loading the
# installed WinFsp DLL by full path.
"Win32_Security_Authorization",
"Win32_System_LibraryLoader",
] }
# WinFsp, for the RAM volume of ephemeral profiles. Donut does not ship the
# driver; the DLL is delay-loaded and used only when it is installed.
winfsp = { version = "0.13", default-features = false }
winfsp-sys = "0.12"
[target.'cfg(windows)'.build-dependencies]
winfsp = { version = "0.13", default-features = false, features = ["delayload"] }
[dev-dependencies]
tempfile = "3.27.0"
+7
View File
@@ -15,6 +15,13 @@ fn main() {
println!("cargo:rustc-link-lib=framework=CoreServices");
}
// WinFsp's DLL loads at the first call into it, so Donut starts on machines
// that do not have WinFsp (src/ramfs).
#[cfg(windows)]
if build_target.contains("windows") {
winfsp::build::winfsp_link_delayload();
}
// Inject build version based on environment variables set by CI
if let Ok(build_tag) = std::env::var("BUILD_TAG") {
// Custom BUILD_TAG takes highest priority (used for nightly builds)
+36
View File
@@ -271,6 +271,31 @@ async fn main() {
.help("Direct path to the Xray worker config JSON file"),
),
)
.subcommand(
Command::new("ramfs")
.about("Serve the in-memory volume of ephemeral profiles until killed (Windows with WinFsp; internal use)")
.arg(
Arg::new("mount")
.long("mount")
.required(true)
.value_parser(clap::value_parser!(std::path::PathBuf))
.help("Directory to mount the volume on; it must not exist"),
)
.arg(
Arg::new("capacity")
.long("capacity")
.required(true)
.value_parser(clap::value_parser!(u64))
.help("Volume size in bytes"),
)
.arg(
Arg::new("state")
.long("state")
.required(true)
.value_parser(clap::value_parser!(std::path::PathBuf))
.help("File that records the mounted volume"),
),
)
.get_matches();
if let Some(proxy_matches) = matches.subcommand_matches("proxy") {
@@ -645,6 +670,17 @@ async fn main() {
);
process::exit(1);
}
} else if let Some(ramfs_matches) = matches.subcommand_matches("ramfs") {
let mount = ramfs_matches
.get_one::<std::path::PathBuf>("mount")
.expect("mount is required");
let capacity = *ramfs_matches
.get_one::<u64>("capacity")
.expect("capacity is required");
let state = ramfs_matches
.get_one::<std::path::PathBuf>("state")
.expect("state is required");
process::exit(donutbrowser_lib::ramfs::host_main(mount, capacity, state));
} else {
eprintln!("No command specified");
process::exit(1);
+5 -1
View File
@@ -680,7 +680,11 @@ impl BrowserRunner {
Some(prepared)
} else {
if profile.ephemeral {
crate::ephemeral_dirs::create_ephemeral_dir(&profile.id.to_string())
// Can mount a RAM disk or start the RAM volume's host and wait for it.
let id = profile.id.to_string();
tokio::task::spawn_blocking(move || crate::ephemeral_dirs::create_ephemeral_dir(&id))
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.to_string().into() })?
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.into() })?;
}
None
+79 -43
View File
@@ -87,7 +87,7 @@ fn earlier_base_exists() -> bool {
#[cfg(target_os = "macos")]
let ram = Path::new(MACOS_RAM_DISK).is_dir();
#[cfg(target_os = "windows")]
let ram = existing_windows_ramdisk().is_some();
let ram = existing_windows_ramdisk().is_some() || crate::ramfs::live_volume().is_some();
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
let ram = false;
ram || disk_fallback_base().exists()
@@ -96,10 +96,11 @@ fn earlier_base_exists() -> bool {
/// Get or create the base directory for ephemeral profiles, and report whether
/// it is actually RAM-backed.
///
/// Linux: /dev/shm (always tmpfs). macOS: RAM disk via hdiutil. Windows: imdisk
/// RAM disk, which is a third-party driver this app does not ship, so on most
/// Windows machines the disk fallback is the normal path rather than an edge
/// case. Callers must treat `Disk` as a downgrade and erase accordingly.
/// Linux: /dev/shm (always tmpfs). macOS: RAM disk via hdiutil. Windows: a RAM
/// disk the person made, else the WinFsp volume (`crate::ramfs`). WinFsp is a
/// driver this app does not ship, so on many Windows machines the disk
/// fallback is the normal path rather than an edge case. Callers must treat
/// `Disk` as a downgrade and erase accordingly.
fn get_ephemeral_base_dir() -> Result<(PathBuf, EphemeralBacking), String> {
if let Some(base) = ephemeral_base_override() {
std::fs::create_dir_all(&base)
@@ -203,44 +204,14 @@ fn existing_windows_ramdisk() -> Option<PathBuf> {
.find(|base| base.is_dir())
}
/// A RAM disk the person made is used as it is. Creating one needs admin
/// rights, so Donut does not try; it mounts its WinFsp volume instead.
#[cfg(target_os = "windows")]
fn get_or_create_windows_ramdisk() -> Result<PathBuf, String> {
if let Some(base) = existing_windows_ramdisk() {
return Ok(base);
}
// Try to create a RAM disk using imdisk (open-source RAM disk driver)
for letter in ['R', 'Q', 'P', 'O'] {
let drive = format!("{}:", letter);
if PathBuf::from(format!("{}\\", drive)).exists() {
continue;
}
let output = std::process::Command::new("imdisk")
.args(["-a", "-s", "256M", "-m", &drive, "-p", "/fs:ntfs /q /y"])
.output();
match output {
Ok(out) if out.status.success() => {
let base = PathBuf::from(format!("{}\\DonutEphemeral", drive));
std::fs::create_dir_all(&base)
.map_err(|e| format!("Failed to create dir on RAM disk: {e}"))?;
log::info!("RAM disk created path={} size_mb=256", base.display());
return Ok(base);
}
Ok(out) => {
log::debug!(
"imdisk failed drive={drive} err=\"{}\"",
String::from_utf8_lossy(&out.stderr).trim()
);
}
Err(e) => {
return Err(format!("imdisk not available: {e}"));
}
}
}
Err("Could not create Windows RAM disk".to_string())
crate::ramfs::ensure_volume()
}
pub fn create_ephemeral_dir(profile_id: &str) -> Result<PathBuf, String> {
@@ -483,20 +454,37 @@ fn sweep_disk_fallback_residue(current_base: &Path) {
}
}
/// Whether the macOS RAM disk is mounted. `None` on other systems.
/// Whether the macOS RAM disk or the Windows RAM volume is mounted. `None`
/// where neither can exist.
pub fn ram_disk_mounted() -> Option<bool> {
#[cfg(target_os = "macos")]
return Some(Path::new(MACOS_RAM_DISK).is_dir());
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "windows")]
return crate::ramfs::winfsp_available()
.ok()
.map(|()| crate::ramfs::live_volume().is_some());
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
None
}
/// Detach the macOS RAM disk when no profile directory is on it. Nothing else
/// detaches it, so it would stay mounted until the Mac restarts.
/// Detach the macOS RAM disk, or stop the Windows RAM volume's host, when no
/// profile directory is on it. Nothing else does, so the macOS disk would stay
/// mounted until the Mac restarts, and the Windows host would keep running.
///
/// Runs at exit, where `hdiutil detach` takes seconds, so the detach is
/// started and not waited for: it finishes after Donut is gone.
pub fn detach_unused_ram_disk() {
#[cfg(target_os = "windows")]
{
if ephemeral_base_override().is_some() {
return;
}
if let Some(volume) = crate::ramfs::live_volume() {
if holds_no_profile_dirs(&volume.mount) {
crate::ramfs::stop_host(volume.pid, "unused_at_exit");
}
}
}
#[cfg(target_os = "macos")]
{
use std::process::Stdio;
@@ -522,7 +510,7 @@ pub fn detach_unused_ram_disk() {
}
/// False when `path` cannot be read, so a missing volume is never detached.
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
fn holds_no_profile_dirs(path: &Path) -> bool {
std::fs::read_dir(path).is_ok_and(|entries| {
entries
@@ -557,6 +545,41 @@ fn cleanup_legacy_dirs() {
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct EphemeralStorageStatus {
in_memory: bool,
/// `winfsp_missing` or `winfsp_error` when not in memory.
reason: Option<&'static str>,
}
/// Whether ephemeral profiles keep their data in memory on this computer.
/// Mounts nothing.
#[tauri::command]
pub async fn get_ephemeral_storage_status() -> EphemeralStorageStatus {
storage_status()
}
fn storage_status() -> EphemeralStorageStatus {
#[cfg(target_os = "windows")]
{
if existing_windows_ramdisk().is_none() {
let reason = match crate::ramfs::status() {
crate::ramfs::Status::InMemory => None,
crate::ramfs::Status::WinfspMissing => Some("winfsp_missing"),
crate::ramfs::Status::WinfspError => Some("winfsp_error"),
};
return EphemeralStorageStatus {
in_memory: reason.is_none(),
reason,
};
}
}
EphemeralStorageStatus {
in_memory: true,
reason: None,
}
}
pub fn get_effective_profile_path(profile: &BrowserProfile, profiles_dir: &Path) -> PathBuf {
if profile.ephemeral || profile.password_protected {
if let Some(dir) = get_ephemeral_dir(&profile.id.to_string()) {
@@ -736,6 +759,19 @@ mod tests {
assert!(get_ephemeral_dir(&id).is_none());
}
#[test]
fn storage_status_is_in_memory_or_names_its_reason() {
let status = serde_json::to_value(storage_status()).unwrap();
let in_memory = status["in_memory"].as_bool().unwrap();
let reason = status["reason"].as_str();
assert_eq!(in_memory, reason.is_none());
#[cfg(not(target_os = "windows"))]
assert!(in_memory);
if let Some(reason) = reason {
assert!(["winfsp_missing", "winfsp_error"].contains(&reason));
}
}
#[test]
#[serial_test::serial]
fn removing_an_unknown_profile_succeeds_without_doing_anything() {
+2
View File
@@ -175,6 +175,7 @@ pub mod proxy_runner;
pub mod proxy_server;
pub mod proxy_storage;
pub mod proxy_udp;
pub mod ramfs;
mod recipes;
mod recorder;
mod remote_exit;
@@ -3725,6 +3726,7 @@ pub fn run_with_builder(
unlock_profile,
lock_profile,
is_profile_locked,
ephemeral_dirs::get_ephemeral_storage_status,
sync::restore_points::list_restore_points,
sync::restore_points::create_restore_point,
sync::restore_points::delete_restore_point,
+447
View File
@@ -0,0 +1,447 @@
//! The process that serves the RAM volume (`donut-proxy ramfs`), and the WinFsp
//! loading it needs. The volume lives exactly as long as this process.
//!
//! WinFsp is a file system driver that Donut does not ship. When it is
//! installed, an ordinary user can mount a user-mode file system on a
//! directory; this process mounts one that keeps everything in its own memory.
use std::os::windows::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use windows::core::{PCWSTR, PWSTR};
use windows::Win32::Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL};
use windows::Win32::Security::{PSECURITY_DESCRIPTOR, TOKEN_QUERY, TOKEN_USER};
use winfsp::host::{FileSystemHost, VolumeParams};
use super::state::VolumeState;
use super::tree::{now, ALLOCATION_UNIT, MAX_NAME_LEN};
use super::winfsp_fs::RamFs;
#[cfg(target_arch = "x86_64")]
pub const DLL_NAME: &str = "winfsp-x64.dll";
#[cfg(target_arch = "aarch64")]
pub const DLL_NAME: &str = "winfsp-a64.dll";
#[cfg(target_arch = "x86")]
pub const DLL_NAME: &str = "winfsp-x86.dll";
/// How long Windows may answer free-space questions from its cache.
const VOLUME_INFO_TIMEOUT_MS: u32 = 1000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unavailable {
Missing,
Broken(String),
}
impl std::fmt::Display for Unavailable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Unavailable::Missing => f.write_str("winfsp not installed"),
Unavailable::Broken(e) => write!(f, "winfsp unusable: {e}"),
}
}
}
fn wide(text: &std::ffi::OsStr) -> Vec<u16> {
text.encode_wide().chain(std::iter::once(0)).collect()
}
fn install_dir() -> Option<PathBuf> {
use winreg::enums::HKEY_LOCAL_MACHINE;
use winreg::RegKey;
let machine = RegKey::predef(HKEY_LOCAL_MACHINE);
["SOFTWARE\\WOW6432Node\\WinFsp", "SOFTWARE\\WinFsp"]
.iter()
.find_map(|key| {
machine
.open_subkey(key)
.ok()?
.get_value::<String, _>("InstallDir")
.ok()
})
.map(PathBuf::from)
}
/// Load the installed WinFsp DLL by its full path, then let the crate bind to
/// it. Nothing may call into WinFsp before this succeeds: the DLL is
/// delay-loaded, and a missing DLL would end the process at the first call.
pub fn load() -> Result<(), Unavailable> {
use windows::Win32::System::LibraryLoader::LoadLibraryW;
static LOADED: OnceLock<()> = OnceLock::new();
if LOADED.get().is_some() {
return Ok(());
}
let dll = install_dir()
.map(|dir| dir.join("bin").join(DLL_NAME))
.filter(|dll| dll.is_file())
.ok_or(Unavailable::Missing)?;
let name = wide(dll.as_os_str());
unsafe { LoadLibraryW(PCWSTR(name.as_ptr())) }
.map_err(|e| Unavailable::Broken(format!("load {}: {e}", dll.display())))?;
winfsp::winfsp_init().map_err(|e| Unavailable::Broken(format!("init: {e:?}")))?;
let _ = LOADED.set(());
Ok(())
}
fn user_sid() -> Result<String, String> {
use windows::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows::Win32::Security::{GetTokenInformation, TokenUser};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
unsafe {
let mut token = HANDLE::default();
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
.map_err(|e| format!("process token: {e}"))?;
let mut len = 0u32;
let _ = GetTokenInformation(token, TokenUser, None, 0, &mut len);
// u64 elements keep the buffer aligned for TOKEN_USER.
let mut buffer = vec![0u64; (len as usize).div_ceil(8)];
let read = GetTokenInformation(
token,
TokenUser,
Some(buffer.as_mut_ptr().cast()),
len,
&mut len,
);
let _ = CloseHandle(token);
read.map_err(|e| format!("token user: {e}"))?;
let user = &*buffer.as_ptr().cast::<TOKEN_USER>();
let mut text = PWSTR::null();
ConvertSidToStringSidW(user.User.Sid, &mut text).map_err(|e| format!("user sid: {e}"))?;
let sid = text.to_string();
let _ = LocalFree(Some(HLOCAL(text.0.cast())));
sid.map_err(|e| format!("user sid: {e}"))
}
}
/// The root directory's descriptor: the user, SYSTEM and Administrators, as in
/// the user's own profile folder, inherited by everything created below.
fn root_security() -> Result<Vec<u8>, String> {
use windows::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
};
let sid = user_sid()?;
let sddl =
format!("O:{sid}G:{sid}D:P(A;OICI;FA;;;S-1-5-18)(A;OICI;FA;;;S-1-5-32-544)(A;OICI;FA;;;{sid})");
let sddl = wide(std::ffi::OsStr::new(&sddl));
let mut descriptor = PSECURITY_DESCRIPTOR::default();
let mut len = 0u32;
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
PCWSTR(sddl.as_ptr()),
SDDL_REVISION_1,
&mut descriptor,
Some(&mut len),
)
.map_err(|e| format!("root security: {e}"))?;
let bytes = std::slice::from_raw_parts(descriptor.0.cast::<u8>(), len as usize).to_vec();
let _ = LocalFree(Some(HLOCAL(descriptor.0)));
Ok(bytes)
}
}
fn volume_params() -> VolumeParams {
let mut params = VolumeParams::new();
params
.sector_size(ALLOCATION_UNIT as u16)
.sectors_per_allocation_unit(1)
.max_component_length(MAX_NAME_LEN as u16)
.volume_creation_time(now())
.volume_serial_number(now() as u32)
.file_info_timeout(u32::MAX)
.case_sensitive_search(false)
.case_preserved_names(true)
.unicode_on_disk(true)
.persistent_acls(true)
.post_cleanup_when_modified_only(true)
.post_disposition_only_when_necessary(true)
.supports_posix_unlink_rename(true)
.reparse_points(true)
.filesystem_name("NTFS");
// Free space must stay current: Chromium sizes its caches and quotas from
// it. The crate sets the timeout but not the flag that makes it count.
// SAFETY: VolumeParams is a transparent wrapper of FSP_FSCTL_VOLUME_PARAMS.
let raw = unsafe {
&mut *(&mut params as *mut VolumeParams).cast::<winfsp_sys::FSP_FSCTL_VOLUME_PARAMS>()
};
raw.set_VolumeInfoTimeoutValid(1);
raw.VolumeInfoTimeout = VOLUME_INFO_TIMEOUT_MS;
params
}
/// A mounted volume. Dropping it unmounts it and frees everything on it.
pub struct Volume {
_host: FileSystemHost<RamFs>,
}
pub fn mount_volume(mount: &Path, capacity: u64) -> Result<Volume, String> {
load().map_err(|e| e.to_string())?;
let fs = RamFs::new(capacity, root_security()?);
let mut host: FileSystemHost<RamFs> =
FileSystemHost::new(volume_params(), fs).map_err(|e| format!("create: {e}"))?;
host
.mount(mount.as_os_str())
.map_err(|e| format!("mount: {e}"))?;
host.start().map_err(|e| format!("start: {e}"))?;
Ok(Volume { _host: host })
}
/// WinFsp creates the mount point itself, so the path must not exist. A
/// crashed host can leave an empty directory or a dead junction there; either
/// is removed. Anything with contents is left alone.
fn clear_mount_point(mount: &Path) -> Result<(), String> {
match std::fs::symlink_metadata(mount) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(format!("mount point unreadable: {e}")),
Ok(_) => std::fs::remove_dir(mount).map_err(|e| format!("mount point in use: {e}")),
}
}
/// Mount, record the volume in `state_file`, and serve until killed.
pub fn host_main(mount: &Path, capacity: u64, state_file: &Path) -> i32 {
let started = std::time::Instant::now();
let pid = std::process::id();
let prepared = mount
.parent()
.map_or(Ok(()), std::fs::create_dir_all)
.map_err(|e| format!("mount parent: {e}"))
.and_then(|()| clear_mount_point(mount));
let volume = match prepared.and_then(|()| mount_volume(mount, capacity)) {
Ok(volume) => volume,
Err(e) => {
log::error!(
"Ram volume mount failed mount={} pid={pid} err=\"{e}\"",
mount.display()
);
return 1;
}
};
let state = VolumeState {
pid,
mount: mount.to_path_buf(),
capacity,
};
if let Err(e) = state.write(state_file) {
log::error!(
"Ram volume state write failed path={} pid={pid} err=\"{e}\"",
state_file.display()
);
return 1;
}
log::info!(
"Ram volume mounted mount={} capacity_mb={} pid={pid} elapsed_ms={}",
mount.display(),
capacity >> 20,
started.elapsed().as_millis()
);
let _volume = volume;
loop {
std::thread::park();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
fn free_bytes(path: &Path) -> u64 {
use windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
let mut name = path.as_os_str().to_os_string();
name.push("\\");
let name = wide(&name);
let mut free = 0u64;
unsafe { GetDiskFreeSpaceExW(PCWSTR(name.as_ptr()), Some(&mut free), None, None) }.unwrap();
free
}
fn write_synced(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut file = std::fs::File::create(path)?;
file.write_all(bytes)?;
file.sync_all()
}
fn wait_for_volume_info() {
std::thread::sleep(std::time::Duration::from_millis(
VOLUME_INFO_TIMEOUT_MS as u64 + 200,
));
}
/// Run after installing WinFsp:
/// `cargo test --lib ramfs::host -- --ignored --nocapture`
#[test]
#[ignore = "needs WinFsp installed"]
fn a_mounted_volume_serves_files_like_a_disk() {
let root = tempfile::tempdir().unwrap();
let mount = root.path().join("ephemeral-ram");
let capacity: u64 = 64 << 20;
let volume = mount_volume(&mount, capacity).unwrap();
let network = mount.join("profile\\Default\\Network");
std::fs::create_dir_all(&network).unwrap();
std::fs::write(network.join("Cookies"), b"a=1").unwrap();
assert_eq!(
std::fs::read(mount.join("PROFILE\\default\\NETWORK\\cookies")).unwrap(),
b"a=1"
);
let listed: Vec<String> = std::fs::read_dir(mount.join("profile\\default"))
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(listed, ["Network"]);
// An atomic write: a temporary file moved over the old one.
let prefs = mount.join("profile\\Default\\Preferences");
let temp = mount.join("profile\\Default\\Preferences.tmp");
std::fs::write(&prefs, b"old").unwrap();
std::fs::write(&temp, b"new").unwrap();
std::fs::rename(&temp, &prefs).unwrap();
assert_eq!(std::fs::read(&prefs).unwrap(), b"new");
assert!(!temp.exists());
// Replace and delete a file that another handle still reads.
let mut held = std::fs::File::open(&prefs).unwrap();
std::fs::write(&temp, b"newer").unwrap();
std::fs::rename(&temp, &prefs).unwrap();
assert_eq!(std::fs::read(&prefs).unwrap(), b"newer");
let mut old = String::new();
held.read_to_string(&mut old).unwrap();
assert_eq!(old, "new");
std::fs::remove_file(&prefs).unwrap();
assert!(!prefs.exists());
drop(held);
// A delete-on-close file goes when its handle closes.
let scratch = mount.join("profile\\Default\\scratch");
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_DELETE_ON_CLOSE: u32 = 0x0400_0000;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.custom_flags(FILE_FLAG_DELETE_ON_CLOSE)
.open(&scratch)
.unwrap();
file.write_all(b"temp").unwrap();
assert!(scratch.exists());
}
assert!(!scratch.exists());
// Windows may refuse to move a directory with an open file in it, as NTFS
// does. Either way the open file keeps its data and one path finds it.
let history = mount.join("profile\\Default\\History");
std::fs::write(&history, b"visits").unwrap();
let mut open_child = std::fs::File::open(&history).unwrap();
let moved = std::fs::rename(
mount.join("profile\\Default"),
mount.join("profile\\Profile 1"),
);
let mut visits = String::new();
open_child.read_to_string(&mut visits).unwrap();
assert_eq!(visits, "visits");
drop(open_child);
let default = match moved {
Ok(()) => mount.join("profile\\Profile 1"),
Err(e) => {
assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied, "{e}");
mount.join("profile\\Default")
}
};
assert_eq!(std::fs::read(default.join("History")).unwrap(), b"visits");
let network = default.join("Network");
// An ACE added to a folder stays and passes to new files, as Chromium's
// sandbox needs for the Network folder. AC is ALL APPLICATION PACKAGES.
let sddl = |path: &Path| {
let script = format!("(Get-Acl -LiteralPath '{}').Sddl", path.display());
let out = std::process::Command::new("powershell")
.args(["-NoProfile", "-Command", &script])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).into_owned()
};
let granted = std::process::Command::new("icacls")
.arg(&network)
.args(["/grant", "*S-1-15-2-1:(OI)(CI)(F)"])
.output()
.unwrap();
assert!(granted.status.success(), "{granted:?}");
let tokens = network.join("Trust Tokens");
std::fs::write(&tokens, b"x").unwrap();
assert!(sddl(&network).contains(";;;AC)"), "{}", sddl(&network));
assert!(sddl(&tokens).contains(";;;AC)"), "{}", sddl(&tokens));
// A plain file answers "not a reparse point".
{
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use windows::Win32::Foundation::ERROR_NOT_A_REPARSE_POINT;
use windows::Win32::System::IO::DeviceIoControl;
const FSCTL_GET_REPARSE_POINT: u32 = 0x0009_00A8;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(network.join("Cookies"))
.unwrap();
let mut buffer = vec![0u8; 16 * 1024];
let mut returned = 0u32;
let queried = unsafe {
DeviceIoControl(
HANDLE(file.as_raw_handle()),
FSCTL_GET_REPARSE_POINT,
None,
0,
Some(buffer.as_mut_ptr().cast()),
buffer.len() as u32,
Some(&mut returned),
None,
)
};
assert_eq!(
queried.unwrap_err().code(),
ERROR_NOT_A_REPARSE_POINT.to_hresult()
);
}
// Free space follows what is written and deleted.
wait_for_volume_info();
let before = free_bytes(&mount);
assert!(before < capacity);
let cache = mount.join("profile\\cache");
write_synced(&cache, &vec![7u8; 8 << 20]).unwrap();
wait_for_volume_info();
let during = free_bytes(&mount);
assert!(
before - during >= 8 << 20,
"before={before} during={during}"
);
std::fs::remove_file(&cache).unwrap();
wait_for_volume_info();
assert_eq!(free_bytes(&mount), before);
let huge = mount.join("profile\\huge");
assert!(write_synced(&huge, &vec![0u8; capacity as usize + 1]).is_err());
let _ = std::fs::remove_file(&huge);
std::fs::remove_dir_all(mount.join("profile")).unwrap();
assert_eq!(std::fs::read_dir(&mount).unwrap().count(), 0);
drop(volume);
assert!(!mount.exists());
}
#[test]
fn a_stale_mount_point_is_cleared_but_contents_are_kept() {
let root = tempfile::tempdir().unwrap();
let mount = root.path().join("ephemeral-ram");
assert!(clear_mount_point(&mount).is_ok());
std::fs::create_dir(&mount).unwrap();
assert!(clear_mount_point(&mount).is_ok());
assert!(!mount.exists());
std::fs::create_dir(&mount).unwrap();
std::fs::write(mount.join("keep"), b"x").unwrap();
assert!(clear_mount_point(&mount).is_err());
assert!(mount.join("keep").exists());
}
}
+303
View File
@@ -0,0 +1,303 @@
//! In-memory storage for ephemeral and password-protected profiles on Windows.
//!
//! Windows has no tmpfs, and the RAM disk drivers that exist need admin rights.
//! WinFsp, when the person has installed it, lets an ordinary user mount a
//! file system served by a process: `donut-proxy ramfs` serves one that keeps
//! every byte in its own memory. It is a separate process because a browser
//! can outlive the app, and its profile must stay readable until it exits.
pub mod state;
pub mod tree;
#[cfg(windows)]
mod host;
#[cfg(windows)]
mod winfsp_fs;
pub use state::VolumeState;
const MIN_CAPACITY: u64 = 512 << 20;
const MAX_CAPACITY: u64 = 4 << 30;
/// A quarter of the RAM, between 512 MiB and 4 GiB.
pub fn volume_capacity(total_ram: u64) -> u64 {
(total_ram / 4).clamp(MIN_CAPACITY, MAX_CAPACITY)
}
/// Why the volume cannot be used, or `Ok` when WinFsp is ready.
pub fn winfsp_available() -> Result<(), String> {
#[cfg(windows)]
{
host::load().map_err(|e| e.to_string())
}
#[cfg(not(windows))]
{
Err("winfsp is for windows only".to_string())
}
}
/// What the ephemeral-storage notice shows.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
InMemory,
WinfspMissing,
WinfspError,
}
/// `donut-proxy ramfs`: mount the volume and serve it until killed.
pub fn host_main(mount: &std::path::Path, capacity: u64, state_file: &std::path::Path) -> i32 {
#[cfg(windows)]
{
host::host_main(mount, capacity, state_file)
}
#[cfg(not(windows))]
{
log::error!(
"Ram volume mount failed mount={} capacity_mb={} state={} err=\"windows only\"",
mount.display(),
capacity >> 20,
state_file.display()
);
2
}
}
#[cfg(windows)]
pub use windows_client::{ensure_volume, live_volume, status, stop_host};
#[cfg(windows)]
mod windows_client {
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, PoisonError};
use std::time::{Duration, Instant};
use super::host::{self, Unavailable};
use super::{volume_capacity, Status, VolumeState};
const MOUNT_WAIT: Duration = Duration::from_secs(10);
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
/// Set when WinFsp loaded but the last start of the volume failed.
static LAST_START_FAILED: AtomicBool = AtomicBool::new(false);
fn mount_dir() -> PathBuf {
crate::app_dirs::data_dir().join("ephemeral-ram")
}
fn state_path() -> PathBuf {
crate::app_dirs::data_dir().join("ephemeral-ram.json")
}
fn log_path() -> PathBuf {
std::env::temp_dir().join("donut-ramfs.log")
}
fn host_alive(pid: u32) -> bool {
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
let pid = sysinfo::Pid::from_u32(pid);
let mut system = System::new();
system.refresh_processes_specifics(
ProcessesToUpdate::Some(&[pid]),
true,
ProcessRefreshKind::nothing(),
);
system.process(pid).is_some_and(|process| {
crate::proxy_storage::snapshot_process_is_alive(process)
&& process
.name()
.to_string_lossy()
.to_ascii_lowercase()
.starts_with("donut-proxy")
})
}
/// A live mount point is a junction into the volume, and it lists.
fn mount_answers(mount: &Path) -> bool {
use std::os::windows::fs::MetadataExt;
std::fs::symlink_metadata(mount)
.is_ok_and(|meta| meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0)
&& std::fs::read_dir(mount).is_ok()
}
/// The volume of this data directory, when its host runs and it answers.
pub fn live_volume() -> Option<VolumeState> {
let state = VolumeState::read(&state_path())?;
(state.mount == mount_dir() && host_alive(state.pid) && mount_answers(&state.mount))
.then_some(state)
}
pub fn status() -> Status {
match host::load() {
Err(Unavailable::Missing) => Status::WinfspMissing,
Err(Unavailable::Broken(_)) => Status::WinfspError,
Ok(()) if LAST_START_FAILED.load(Ordering::Relaxed) => Status::WinfspError,
Ok(()) => Status::InMemory,
}
}
/// The mounted volume's path, starting its host when no live one exists.
pub fn ensure_volume() -> Result<PathBuf, String> {
static STARTING: Mutex<()> = Mutex::new(());
let _starting = STARTING.lock().unwrap_or_else(PoisonError::into_inner);
host::load().map_err(|e| e.to_string())?;
if live_volume().is_some() {
LAST_START_FAILED.store(false, Ordering::Relaxed);
return Ok(mount_dir());
}
let started = start_host();
LAST_START_FAILED.store(started.is_err(), Ordering::Relaxed);
started
}
fn start_host() -> Result<PathBuf, String> {
use std::os::windows::process::CommandExt;
use std::process::{Command, Stdio};
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let started = Instant::now();
let mount = mount_dir();
let capacity = {
let mut system = sysinfo::System::new();
system.refresh_memory();
volume_capacity(system.total_memory())
};
let exe = crate::proxy_runner::find_sidecar_executable("donut-proxy")
.map_err(|e| format!("donut-proxy not found: {e}"))?;
let log =
crate::app_dirs::create_owner_only(&log_path()).map_or_else(|_| Stdio::null(), Stdio::from);
let mut child = Command::new(exe)
.arg("ramfs")
.arg("--mount")
.arg(&mount)
.arg("--capacity")
.arg(capacity.to_string())
.arg("--state")
.arg(state_path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(log)
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW)
.spawn()
.map_err(|e| format!("ramfs host spawn: {e}"))?;
let pid = child.id();
loop {
if live_volume().is_some_and(|state| state.pid == pid) {
log::info!(
"Ram volume host started pid={pid} mount={} capacity_mb={} elapsed_ms={}",
mount.display(),
capacity >> 20,
started.elapsed().as_millis()
);
return Ok(mount);
}
if let Ok(Some(status)) = child.try_wait() {
return Err(format!(
"ramfs host exited ({status}) log={}",
log_path().display()
));
}
if started.elapsed() > MOUNT_WAIT {
let _ = child.kill();
return Err(format!(
"ramfs host did not mount in {}s log={}",
MOUNT_WAIT.as_secs(),
log_path().display()
));
}
std::thread::sleep(Duration::from_millis(100));
}
}
/// End the host, which unmounts the volume and frees its memory. Not waited
/// for, because this runs at exit.
pub fn stop_host(pid: u32, reason: &str) {
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
let stopped = unsafe {
OpenProcess(PROCESS_TERMINATE, false, pid).and_then(|process| {
let result = TerminateProcess(process, 0);
let _ = CloseHandle(process);
result
})
};
match stopped {
Ok(()) => {
let _ = std::fs::remove_file(state_path());
log::info!("Ram volume host stopped pid={pid} reason={reason}");
}
Err(e) => log::warn!("Ram volume host stop failed pid={pid} reason={reason} err=\"{e}\""),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_missing_winfsp_is_reported_and_never_called() {
match host::load() {
Ok(()) => assert_ne!(status(), Status::WinfspMissing),
Err(Unavailable::Missing) => {
assert_eq!(status(), Status::WinfspMissing);
assert_eq!(ensure_volume(), Err("winfsp not installed".to_string()));
}
Err(Unavailable::Broken(e)) => panic!("{e}"),
}
}
/// Run after installing WinFsp and building the sidecar:
/// `cargo build --bin donut-proxy`, then
/// `cargo test --lib ramfs::windows_client -- --ignored --nocapture`
#[test]
#[ignore = "needs WinFsp installed and a built donut-proxy"]
fn the_sidecar_host_serves_a_volume_until_stopped() {
let data = tempfile::tempdir().unwrap();
let _data = crate::app_dirs::set_test_data_dir(data.path().to_path_buf());
let mount = ensure_volume().unwrap();
assert_eq!(mount, data.path().join("ephemeral-ram"));
assert_eq!(status(), Status::InMemory);
std::fs::write(mount.join("probe"), b"x").unwrap();
let volume = live_volume().unwrap();
assert_eq!(ensure_volume().unwrap(), mount);
assert_eq!(live_volume().unwrap().pid, volume.pid);
assert_eq!(std::fs::read(mount.join("probe")).unwrap(), b"x");
stop_host(volume.pid, "test");
let deadline = Instant::now() + Duration::from_secs(5);
while live_volume().is_some() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(100));
}
assert!(live_volume().is_none());
assert!(!mount.join("probe").exists());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capacity_is_a_quarter_of_ram_within_bounds() {
assert_eq!(volume_capacity(1 << 30), MIN_CAPACITY);
assert_eq!(volume_capacity(8 << 30), 2 << 30);
assert_eq!(volume_capacity(64 << 30), MAX_CAPACITY);
assert_eq!(volume_capacity(0), MIN_CAPACITY);
}
#[cfg(not(windows))]
#[test]
fn other_systems_have_no_volume_host() {
assert!(winfsp_available().is_err());
let dir = tempfile::tempdir().unwrap();
assert_eq!(
host_main(&dir.path().join("m"), MIN_CAPACITY, &dir.path().join("s")),
2
);
}
}
+50
View File
@@ -0,0 +1,50 @@
//! The file the volume host writes once its volume is mounted, so the app can
//! find a volume that an earlier run started.
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VolumeState {
pub pid: u32,
pub mount: PathBuf,
pub capacity: u64,
}
impl VolumeState {
/// Written to a temporary file first, so a reader never sees half of it.
pub fn write(&self, path: &Path) -> std::io::Result<()> {
let temporary = path.with_extension("tmp");
std::fs::write(&temporary, serde_json::to_vec(self)?)?;
std::fs::rename(&temporary, path)
}
pub fn read(path: &Path) -> Option<Self> {
serde_json::from_slice(&std::fs::read(path).ok()?).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn state_round_trips_and_unreadable_state_is_none() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ephemeral-ram.json");
assert_eq!(VolumeState::read(&path), None);
let state = VolumeState {
pid: 42,
mount: dir.path().join("ephemeral-ram"),
capacity: 512 << 20,
};
state.write(&path).unwrap();
assert_eq!(VolumeState::read(&path), Some(state));
assert!(!path.with_extension("tmp").exists());
std::fs::write(&path, b"{").unwrap();
assert_eq!(VolumeState::read(&path), None);
}
}
File diff suppressed because it is too large. Load diff
+511
View File
@@ -0,0 +1,511 @@
//! The WinFsp side of the RAM volume: each request maps onto `Tree`.
//!
//! WinFsp's kernel driver handles byte-range locks, memory-mapped files, the
//! cache and change notifications; this layer keeps names, data and security
//! descriptors.
use std::ffi::c_void;
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
use windows::Win32::Foundation::{
STATUS_ACCESS_DENIED, STATUS_BUFFER_OVERFLOW, STATUS_CANNOT_DELETE, STATUS_DIRECTORY_NOT_EMPTY,
STATUS_DISK_FULL, STATUS_END_OF_FILE, STATUS_FILE_IS_A_DIRECTORY, STATUS_INSUFFICIENT_RESOURCES,
STATUS_INVALID_DEVICE_REQUEST, STATUS_INVALID_PARAMETER, STATUS_NOT_A_DIRECTORY,
STATUS_NOT_A_REPARSE_POINT, STATUS_OBJECT_NAME_COLLISION, STATUS_OBJECT_NAME_INVALID,
STATUS_OBJECT_NAME_NOT_FOUND, STATUS_OBJECT_PATH_NOT_FOUND,
};
use windows::Win32::Security::{GetSecurityDescriptorLength, PSECURITY_DESCRIPTOR};
use winfsp::constants::FspCleanupFlags;
use winfsp::filesystem::{
DirInfo, DirMarker, FileInfo, FileSecurity, FileSystemContext, ModificationDescriptor,
OpenFileInfo, VolumeInfo, WideNameInfo,
};
use winfsp::{FspError, U16CStr};
use winfsp_sys::{FILE_ACCESS_RIGHTS, FILE_FLAGS_AND_ATTRIBUTES};
use super::tree::{BasicInfo, FsError, Info, NodeId, Tree, ROOT};
const FILE_DIRECTORY_FILE: u32 = 0x1;
const FILE_NON_DIRECTORY_FILE: u32 = 0x40;
const INVALID_FILE_ATTRIBUTES: u32 = u32::MAX;
const VOLUME_LABEL: &str = "DonutEphemeral";
/// WinFsp reads a security descriptor by name into a 1024-byte buffer, and this
/// binding cannot report that more room is needed. Larger descriptors are
/// refused when they are set, so no open ever fails on one.
pub const MAX_SECURITY_LEN: usize = 1024;
impl From<FsError> for FspError {
fn from(error: FsError) -> Self {
let status = match error {
FsError::NotFound => STATUS_OBJECT_NAME_NOT_FOUND,
FsError::PathNotFound => STATUS_OBJECT_PATH_NOT_FOUND,
FsError::NameCollision => STATUS_OBJECT_NAME_COLLISION,
FsError::NameInvalid => STATUS_OBJECT_NAME_INVALID,
FsError::NotADirectory => STATUS_NOT_A_DIRECTORY,
FsError::IsADirectory => STATUS_FILE_IS_A_DIRECTORY,
FsError::DirectoryNotEmpty => STATUS_DIRECTORY_NOT_EMPTY,
FsError::AccessDenied => STATUS_ACCESS_DENIED,
FsError::CannotDelete => STATUS_CANNOT_DELETE,
FsError::DiskFull => STATUS_DISK_FULL,
FsError::EndOfFile => STATUS_END_OF_FILE,
FsError::InvalidParameter => STATUS_INVALID_PARAMETER,
};
FspError::NTSTATUS(status.0)
}
}
fn status(status: windows::Win32::Foundation::NTSTATUS) -> FspError {
FspError::NTSTATUS(status.0)
}
fn fill(out: &mut FileInfo, info: Info) {
out.file_attributes = info.attributes;
out.reparse_tag = 0;
out.allocation_size = info.allocation_size;
out.file_size = info.file_size;
out.creation_time = info.creation_time;
out.last_access_time = info.last_access_time;
out.last_write_time = info.last_write_time;
out.change_time = info.change_time;
out.index_number = info.index_number;
out.hard_links = 0;
out.ea_size = 0;
}
/// One open of a file or directory.
pub struct Handle {
id: NodeId,
}
pub struct RamFs {
tree: Mutex<Tree>,
}
impl RamFs {
pub fn new(capacity: u64, root_security: Vec<u8>) -> Self {
RamFs {
tree: Mutex::new(Tree::new(capacity, root_security)),
}
}
/// A callback that panicked poisons the lock; the tree stays consistent
/// because every change finishes before the next can fail.
fn tree(&self) -> MutexGuard<'_, Tree> {
self.tree.lock().unwrap_or_else(PoisonError::into_inner)
}
fn opened(
&self,
tree: &Tree,
id: NodeId,
file_info: &mut OpenFileInfo,
) -> winfsp::Result<Handle> {
fill(file_info.as_mut(), tree.info(id)?);
// The stored case of every name, so Windows reports what was created.
let path = tree.full_path(id)?;
if path.len() * 2 < file_info.normalized_name_size() as usize {
file_info.set_normalized_name(&path, None);
}
Ok(Handle { id })
}
}
/// The address of `FspSetSecurityDescriptor` inside the WinFsp DLL. WinFsp
/// frees a descriptor by comparing the function that made it to its own
/// exports, and the delay-load stub this binary links has another address.
fn set_security_descriptor_export() -> Option<unsafe extern "C" fn() -> i32> {
use windows::core::{PCSTR, PCWSTR};
use windows::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
static EXPORT: OnceLock<Option<unsafe extern "C" fn() -> i32>> = OnceLock::new();
*EXPORT.get_or_init(|| {
let name: Vec<u16> = super::host::DLL_NAME
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let module = unsafe { GetModuleHandleW(PCWSTR(name.as_ptr())) }.ok()?;
let export =
unsafe { GetProcAddress(module, PCSTR(c"FspSetSecurityDescriptor".as_ptr().cast())) }?;
// SAFETY: WinFsp's FspDeleteSecurityDescriptor takes this function as an
// untyped `NTSTATUS (*)()` and only compares it.
Some(unsafe {
std::mem::transmute::<unsafe extern "system" fn() -> isize, unsafe extern "C" fn() -> i32>(
export,
)
})
})
}
/// Apply a SetSecurity request to a stored descriptor.
fn modified_security(
current: &[u8],
security_information: u32,
modification: &ModificationDescriptor,
) -> winfsp::Result<Vec<u8>> {
let export = set_security_descriptor_export().ok_or(status(STATUS_INVALID_DEVICE_REQUEST))?;
// The input parameter is not const, so it gets a private copy.
let mut input = current.to_vec();
let mut output: winfsp_sys::PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let result = unsafe {
winfsp_sys::FspSetSecurityDescriptor(
input.as_mut_ptr().cast(),
security_information,
modification.as_mut_ptr(),
&mut output,
)
};
if result < 0 {
return Err(FspError::NTSTATUS(result));
}
let len = unsafe { GetSecurityDescriptorLength(PSECURITY_DESCRIPTOR(output)) } as usize;
let updated = unsafe { std::slice::from_raw_parts(output.cast::<u8>(), len) }.to_vec();
unsafe { winfsp_sys::FspDeleteSecurityDescriptor(output, Some(export)) };
Ok(updated)
}
impl FileSystemContext for RamFs {
type FileContext = Handle;
fn get_security_by_name(
&self,
file_name: &U16CStr,
security_descriptor: Option<&mut [c_void]>,
_reparse_point_resolver: impl FnOnce(&U16CStr) -> Option<FileSecurity>,
) -> winfsp::Result<FileSecurity> {
let tree = self.tree();
let id = tree.lookup(file_name.as_slice())?;
let attributes = tree.info(id)?.attributes;
let security = tree.security(id)?;
if let Some(buffer) = security_descriptor {
if buffer.len() < security.len() {
// Never STATUS_BUFFER_OVERFLOW: WinFsp would retry with the same size.
return Err(status(STATUS_INSUFFICIENT_RESOURCES));
}
unsafe {
std::ptr::copy_nonoverlapping(
security.as_ptr(),
buffer.as_mut_ptr().cast::<u8>(),
security.len(),
)
};
}
Ok(FileSecurity {
reparse: false,
sz_security_descriptor: security.len() as u64,
attributes,
})
}
fn open(
&self,
file_name: &U16CStr,
create_options: u32,
_granted_access: FILE_ACCESS_RIGHTS,
file_info: &mut OpenFileInfo,
) -> winfsp::Result<Handle> {
let mut tree = self.tree();
let id = tree.lookup(file_name.as_slice())?;
let is_dir = tree.is_dir(id)?;
if create_options & FILE_DIRECTORY_FILE != 0 && !is_dir {
return Err(FsError::NotADirectory.into());
}
if create_options & FILE_NON_DIRECTORY_FILE != 0 && is_dir {
return Err(FsError::IsADirectory.into());
}
tree.open(id)?;
self.opened(&tree, id, file_info)
}
fn close(&self, context: Handle) {
self.tree().close(context.id);
}
fn create(
&self,
file_name: &U16CStr,
create_options: u32,
_granted_access: FILE_ACCESS_RIGHTS,
file_attributes: FILE_FLAGS_AND_ATTRIBUTES,
security_descriptor: Option<&[c_void]>,
allocation_size: u64,
extra_buffer: Option<&[u8]>,
extra_buffer_is_reparse_point: bool,
file_info: &mut OpenFileInfo,
) -> winfsp::Result<Handle> {
if extra_buffer_is_reparse_point && extra_buffer.is_some_and(|buffer| !buffer.is_empty()) {
return Err(status(STATUS_ACCESS_DENIED));
}
let mut tree = self.tree();
let security = match security_descriptor {
Some(descriptor) => {
unsafe { std::slice::from_raw_parts(descriptor.as_ptr().cast::<u8>(), descriptor.len()) }
.to_vec()
}
None => tree.security(ROOT)?.to_vec(),
};
if security.len() > MAX_SECURITY_LEN {
return Err(status(STATUS_INSUFFICIENT_RESOURCES));
}
let id = tree.create(
file_name.as_slice(),
create_options & FILE_DIRECTORY_FILE != 0,
file_attributes,
security,
allocation_size,
)?;
self.opened(&tree, id, file_info)
}
fn cleanup(&self, context: &Handle, _file_name: Option<&U16CStr>, flags: u32) {
let mut tree = self.tree();
let flagged = |flag: FspCleanupFlags| flag.is_flagged(flags);
if flagged(FspCleanupFlags::FspCleanupSetAllocationSize) {
let _ = tree.trim_allocation(context.id);
}
let _ = tree.touch(
context.id,
flagged(FspCleanupFlags::FspCleanupSetLastAccessTime),
flagged(FspCleanupFlags::FspCleanupSetLastWriteTime),
flagged(FspCleanupFlags::FspCleanupSetChangeTime),
flagged(FspCleanupFlags::FspCleanupSetArchiveBit),
);
if flagged(FspCleanupFlags::FspCleanupDelete) {
tree.unlink(context.id);
}
}
fn flush(&self, context: Option<&Handle>, file_info: &mut FileInfo) -> winfsp::Result<()> {
if let Some(context) = context {
fill(file_info, self.tree().info(context.id)?);
}
Ok(())
}
fn get_file_info(&self, context: &Handle, file_info: &mut FileInfo) -> winfsp::Result<()> {
fill(file_info, self.tree().info(context.id)?);
Ok(())
}
fn get_security(
&self,
context: &Handle,
security_descriptor: Option<&mut [c_void]>,
) -> winfsp::Result<u64> {
let tree = self.tree();
let security = tree.security(context.id)?;
if let Some(buffer) = security_descriptor {
if buffer.len() < security.len() {
return Err(status(STATUS_BUFFER_OVERFLOW));
}
unsafe {
std::ptr::copy_nonoverlapping(
security.as_ptr(),
buffer.as_mut_ptr().cast::<u8>(),
security.len(),
)
};
}
Ok(security.len() as u64)
}
fn set_security(
&self,
context: &Handle,
security_information: u32,
modification_descriptor: ModificationDescriptor,
) -> winfsp::Result<()> {
let mut tree = self.tree();
let updated = modified_security(
tree.security(context.id)?,
security_information,
&modification_descriptor,
)?;
if updated.len() > MAX_SECURITY_LEN {
return Err(status(STATUS_INSUFFICIENT_RESOURCES));
}
tree.set_security(context.id, updated)?;
Ok(())
}
fn overwrite(
&self,
context: &Handle,
file_attributes: FILE_FLAGS_AND_ATTRIBUTES,
replace_file_attributes: bool,
allocation_size: u64,
_extra_buffer: Option<&[u8]>,
file_info: &mut FileInfo,
) -> winfsp::Result<()> {
let mut tree = self.tree();
tree.overwrite(
context.id,
file_attributes,
replace_file_attributes,
allocation_size,
)?;
fill(file_info, tree.info(context.id)?);
Ok(())
}
fn read_directory(
&self,
context: &Handle,
_pattern: Option<&U16CStr>,
marker: DirMarker,
buffer: &mut [u8],
) -> winfsp::Result<u32> {
let tree = self.tree();
let mut cursor = 0u32;
let mut entry: DirInfo = DirInfo::new();
let mut full = false;
tree.for_each_entry(context.id, marker.inner(), |name, info| {
entry.reset();
fill(entry.file_info_mut(), info);
if entry.set_name_raw(name).is_err() {
return true;
}
if !entry.append_to_buffer(buffer, &mut cursor) {
full = true;
return false;
}
true
})?;
if !full {
<DirInfo as WideNameInfo>::finalize_buffer(buffer, &mut cursor);
}
Ok(cursor)
}
fn rename(
&self,
context: &Handle,
_file_name: &U16CStr,
new_file_name: &U16CStr,
replace_if_exists: bool,
) -> winfsp::Result<()> {
self
.tree()
.rename(context.id, new_file_name.as_slice(), replace_if_exists)?;
Ok(())
}
fn set_basic_info(
&self,
context: &Handle,
file_attributes: u32,
creation_time: u64,
last_access_time: u64,
last_write_time: u64,
last_change_time: u64,
file_info: &mut FileInfo,
) -> winfsp::Result<()> {
let set = |time: u64| (time != 0).then_some(time);
let mut tree = self.tree();
tree.set_basic(
context.id,
BasicInfo {
attributes: (file_attributes != INVALID_FILE_ATTRIBUTES).then_some(file_attributes),
creation_time: set(creation_time),
last_access_time: set(last_access_time),
last_write_time: set(last_write_time),
change_time: set(last_change_time),
},
)?;
fill(file_info, tree.info(context.id)?);
Ok(())
}
fn set_delete(
&self,
context: &Handle,
_file_name: &U16CStr,
delete_file: bool,
) -> winfsp::Result<()> {
if delete_file {
self.tree().can_delete(context.id)?;
}
Ok(())
}
fn set_file_size(
&self,
context: &Handle,
new_size: u64,
set_allocation_size: bool,
file_info: &mut FileInfo,
) -> winfsp::Result<()> {
let mut tree = self.tree();
if set_allocation_size {
tree.set_allocation_size(context.id, new_size)?;
} else {
tree.set_file_size(context.id, new_size)?;
}
fill(file_info, tree.info(context.id)?);
Ok(())
}
fn read(&self, context: &Handle, buffer: &mut [u8], offset: u64) -> winfsp::Result<u32> {
Ok(self.tree().read(context.id, offset, buffer)? as u32)
}
fn write(
&self,
context: &Handle,
buffer: &[u8],
offset: u64,
write_to_eof: bool,
constrained_io: bool,
file_info: &mut FileInfo,
) -> winfsp::Result<u32> {
let mut tree = self.tree();
let written = tree.write(context.id, offset, buffer, write_to_eof, constrained_io)?;
fill(file_info, tree.info(context.id)?);
Ok(written as u32)
}
fn get_volume_info(&self, out_volume_info: &mut VolumeInfo) -> winfsp::Result<()> {
let tree = self.tree();
out_volume_info.total_size = tree.capacity();
out_volume_info.free_size = tree.free();
out_volume_info.set_volume_label(VOLUME_LABEL);
Ok(())
}
// The volume holds no reparse points, but it answers for them as NTFS does,
// because callers treat "not a reparse point" and "not supported" apart.
fn get_reparse_point_by_name(
&self,
_file_name: &U16CStr,
_is_directory: bool,
_buffer: &mut [u8],
) -> winfsp::Result<u64> {
Err(status(STATUS_NOT_A_REPARSE_POINT))
}
fn get_reparse_point(
&self,
_context: &Handle,
_file_name: &U16CStr,
_buffer: &mut [u8],
) -> winfsp::Result<u64> {
Err(status(STATUS_NOT_A_REPARSE_POINT))
}
fn set_reparse_point(
&self,
_context: &Handle,
_file_name: &U16CStr,
_buffer: &[u8],
) -> winfsp::Result<()> {
Err(status(STATUS_ACCESS_DENIED))
}
fn delete_reparse_point(
&self,
_context: &Handle,
_file_name: &U16CStr,
_buffer: &[u8],
) -> winfsp::Result<()> {
Err(status(STATUS_NOT_A_REPARSE_POINT))
}
}
+3
View File
@@ -26,6 +26,7 @@ import {
LuTriangleAlert,
} from "react-icons/lu";
import { DnsServerField } from "@/components/dns-server-field";
import { EphemeralStorageNotice } from "@/components/ephemeral-storage-notice";
import { LoadingButton } from "@/components/loading-button";
import { ProfileGlyph } from "@/components/profile-glyph";
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
@@ -1088,6 +1089,8 @@ export function CreateProfileDialog({
/>
</motion.div>
{(ephemeral || enablePassword) && <EphemeralStorageNotice />}
{enablePassword && (
<motion.div className="space-y-1.5" {...reveal}>
<div className="grid gap-2 sm:grid-cols-2">
@@ -0,0 +1,65 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuTriangleAlert } from "react-icons/lu";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface EphemeralStorageStatus {
in_memory: boolean;
reason: "winfsp_missing" | "winfsp_error" | null;
}
const WINFSP_DOWNLOAD_URL = "https://winfsp.dev/rel/";
/** Shown when this computer cannot keep ephemeral profiles in memory. */
export function EphemeralStorageNotice({ className }: { className?: string }) {
const { t } = useTranslation();
const [status, setStatus] = useState<EphemeralStorageStatus | null>(null);
useEffect(() => {
let active = true;
invoke<EphemeralStorageStatus>("get_ephemeral_storage_status")
.then((next) => {
if (active) setStatus(next);
})
.catch((error: unknown) => {
console.error("Failed to read ephemeral storage status", error);
});
return () => {
active = false;
};
}, []);
if (!status || status.in_memory) return null;
return (
<div
role="note"
className={cn(
"flex items-start gap-2 text-xs text-warning-text",
className,
)}
>
<LuTriangleAlert className="mt-0.5 size-3.5 shrink-0" aria-hidden />
<p>
{t(
status.reason === "winfsp_error"
? "ephemeralStorage.winfspError"
: "ephemeralStorage.onDisk",
)}{" "}
<Button
type="button"
variant="link"
className="h-auto p-0 text-xs"
onClick={() => void openUrl(WINFSP_DOWNLOAD_URL)}
>
{t("ephemeralStorage.download")}
</Button>
</p>
</div>
);
}
+16
View File
@@ -2667,6 +2667,10 @@
"name": "stable_deref_trait",
"license": "Apache-2.0 OR MIT"
},
{
"name": "static_assertions",
"license": "Apache-2.0 OR MIT"
},
{
"name": "strict-num",
"license": "MIT"
@@ -3155,6 +3159,10 @@
"name": "weezl",
"license": "Apache-2.0 OR MIT"
},
{
"name": "widestring",
"license": "Apache-2.0 OR MIT"
},
{
"name": "winapi",
"license": "Apache-2.0 OR MIT"
@@ -3231,6 +3239,14 @@
"name": "windows_x86_64_msvc",
"license": "Apache-2.0 OR MIT"
},
{
"name": "winfsp",
"license": "GPL-3.0"
},
{
"name": "winfsp-sys",
"license": "GPL-3.0"
},
{
"name": "winnow",
"license": "MIT"
+5
View File
@@ -4721,5 +4721,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} startet ohne die eigenen Fingerprint-Änderungen",
"fingerprintEditsNeedPro": "Deine Änderungen bleiben gespeichert und gelten wieder mit einem aktiven Pro-Abo."
},
"ephemeralStorage": {
"onDisk": "Auf diesem Computer schreiben flüchtige Profile ihre Daten auf den Datenträger. Donut löscht sie, wenn der Browser geschlossen wird. Installieren Sie WinFsp, um die Daten im Arbeitsspeicher zu halten.",
"winfspError": "WinFsp ist nicht gestartet, daher schreiben flüchtige Profile ihre Daten auf den Datenträger. Donut löscht sie, wenn der Browser geschlossen wird.",
"download": "WinFsp herunterladen"
}
}
+5
View File
@@ -4721,5 +4721,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} launches without its custom fingerprint changes",
"fingerprintEditsNeedPro": "Your changes are kept and apply again with an active Pro plan."
},
"ephemeralStorage": {
"onDisk": "On this computer, ephemeral profiles write their data to disk. Donut erases it when the browser closes. Install WinFsp to keep the data in memory.",
"winfspError": "WinFsp did not start, so ephemeral profiles write their data to disk. Donut erases it when the browser closes.",
"download": "Download WinFsp"
}
}
+5
View File
@@ -4771,5 +4771,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} se inicia sin sus cambios personalizados de huella digital",
"fingerprintEditsNeedPro": "Tus cambios se conservan y vuelven a aplicarse con un plan Pro activo."
},
"ephemeralStorage": {
"onDisk": "En este equipo, los perfiles efímeros escriben sus datos en el disco. Donut los borra cuando se cierra el navegador. Instala WinFsp para mantener los datos en memoria.",
"winfspError": "WinFsp no se inició, así que los perfiles efímeros escriben sus datos en el disco. Donut los borra cuando se cierra el navegador.",
"download": "Descargar WinFsp"
}
}
+5
View File
@@ -4771,5 +4771,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} démarre sans ses modifications d'empreinte personnalisées",
"fingerprintEditsNeedPro": "Vos modifications sont conservées et s'appliquent de nouveau avec un forfait Pro actif."
},
"ephemeralStorage": {
"onDisk": "Sur cet ordinateur, les profils éphémères écrivent leurs données sur le disque. Donut les efface à la fermeture du navigateur. Installez WinFsp pour garder les données en mémoire.",
"winfspError": "WinFsp n'a pas démarré, donc les profils éphémères écrivent leurs données sur le disque. Donut les efface à la fermeture du navigateur.",
"download": "Télécharger WinFsp"
}
}
+5
View File
@@ -4721,5 +4721,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} はカスタムのフィンガープリント変更なしで起動します",
"fingerprintEditsNeedPro": "変更は保存されており、有効な Pro プランで再び適用されます。"
},
"ephemeralStorage": {
"onDisk": "このコンピューターでは、一時的なプロファイルはデータをディスクに書き込みます。ブラウザを閉じると Donut がそのデータを消去します。データをメモリに保持するには WinFsp をインストールしてください。",
"winfspError": "WinFsp が起動しなかったため、一時的なプロファイルはデータをディスクに書き込みます。ブラウザを閉じると Donut がそのデータを消去します。",
"download": "WinFsp をダウンロード"
}
}
+5
View File
@@ -4721,5 +4721,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}}은(는) 사용자 지정 지문 변경 없이 실행됩니다",
"fingerprintEditsNeedPro": "변경 사항은 보관되며 활성 Pro 플랜에서 다시 적용됩니다."
},
"ephemeralStorage": {
"onDisk": "이 컴퓨터에서는 임시 프로필이 데이터를 디스크에 기록합니다. 브라우저를 닫으면 Donut이 데이터를 지웁니다. 데이터를 메모리에 유지하려면 WinFsp를 설치하세요.",
"winfspError": "WinFsp가 시작되지 않아 임시 프로필이 데이터를 디스크에 기록합니다. 브라우저를 닫으면 Donut이 데이터를 지웁니다.",
"download": "WinFsp 다운로드"
}
}
+5
View File
@@ -4771,5 +4771,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} inicia sem as alterações personalizadas de impressão digital",
"fingerprintEditsNeedPro": "Suas alterações ficam guardadas e voltam a valer com um plano Pro ativo."
},
"ephemeralStorage": {
"onDisk": "Neste computador, os perfis efêmeros gravam seus dados no disco. O Donut os apaga quando o navegador fecha. Instale o WinFsp para manter os dados na memória.",
"winfspError": "O WinFsp não iniciou, então os perfis efêmeros gravam seus dados no disco. O Donut os apaga quando o navegador fecha.",
"download": "Baixar o WinFsp"
}
}
+5
View File
@@ -4821,5 +4821,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} запускается без ваших изменений отпечатка",
"fingerprintEditsNeedPro": "Изменения сохранены и снова применятся с активным тарифом Pro."
},
"ephemeralStorage": {
"onDisk": "На этом компьютере временные профили записывают данные на диск. Donut стирает их, когда браузер закрывается. Установите WinFsp, чтобы хранить данные в памяти.",
"winfspError": "WinFsp не запустился, поэтому временные профили записывают данные на диск. Donut стирает их, когда браузер закрывается.",
"download": "Скачать WinFsp"
}
}
+5
View File
@@ -4721,5 +4721,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} özel parmak izi değişiklikleri olmadan başlatılıyor",
"fingerprintEditsNeedPro": "Değişiklikleriniz saklanır ve etkin bir Pro planıyla yeniden uygulanır."
},
"ephemeralStorage": {
"onDisk": "Bu bilgisayarda geçici profiller verilerini diske yazar. Donut, tarayıcı kapandığında bu verileri siler. Verileri bellekte tutmak için WinFsp'yi yükleyin.",
"winfspError": "WinFsp başlamadı, bu yüzden geçici profiller verilerini diske yazar. Donut, tarayıcı kapandığında bu verileri siler.",
"download": "WinFsp'yi indir"
}
}
+5
View File
@@ -4721,5 +4721,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} khởi chạy mà không có các thay đổi vân tay tùy chỉnh",
"fingerprintEditsNeedPro": "Các thay đổi của bạn được giữ lại và sẽ áp dụng lại khi có gói Pro đang hoạt động."
},
"ephemeralStorage": {
"onDisk": "Trên máy tính này, hồ sơ tạm thời ghi dữ liệu vào ổ đĩa. Donut xóa dữ liệu đó khi trình duyệt đóng. Hãy cài WinFsp để giữ dữ liệu trong bộ nhớ.",
"winfspError": "WinFsp không khởi động được, nên hồ sơ tạm thời ghi dữ liệu vào ổ đĩa. Donut xóa dữ liệu đó khi trình duyệt đóng.",
"download": "Tải WinFsp"
}
}
+5
View File
@@ -4721,5 +4721,10 @@
"launchNotice": {
"fingerprintEditsNotApplied": "{{name}} 将在不含自定义指纹更改的情况下启动",
"fingerprintEditsNeedPro": "你的更改已保留,在 Pro 套餐有效时会再次生效。"
},
"ephemeralStorage": {
"onDisk": "在这台电脑上,临时配置文件会把数据写入磁盘。浏览器关闭时 Donut 会清除这些数据。安装 WinFsp 可将数据保留在内存中。",
"winfspError": "WinFsp 未能启动,因此临时配置文件会把数据写入磁盘。浏览器关闭时 Donut 会清除这些数据。",
"download": "下载 WinFsp"
}
}