commit 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd Author: Sergio Date: Fri May 8 04:45:44 2026 +0000 chore: monorepo inicial con arje + minga + yahweh absorbidos Workspace en 4 ejes (core/modules/apps/shared): - core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero, ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot, ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat) - modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST, minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli) - modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers}, widgets/{tree,splitter,tabs,tiled,container_core,text_input}) - apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer, image_viewer, yahweh-shell) - shared_wit/protocol.wit: handshake/lifecycle inicial Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio "full", paths intra-workspace de yahweh redirigidos a su nueva ubicación. cargo check --workspace: 0 errores, 17 warnings (dead code preexistente). Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..113ac98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock.bak +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2bc8f33 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,10057 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ash-window" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" +dependencies = [ + "ash", + "raw-window-handle", + "raw-window-metal", +] + +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.4", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.12", + "zbus 5.15.0", +] + +[[package]] +name = "ashpd" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a3c86f3fd70c0ffa500ed189abfa90b5a52398a45d5dc372fcc38ebeb7a645" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.4", + "serde", + "serde_repr", + "url", + "zbus 5.15.0", +] + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.4.1", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite 2.6.1", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite 2.6.1", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.1", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async_zip" +version = "0.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite 2.6.1", + "pin-project", + "thiserror 1.0.69", +] + +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64", + "http", + "log", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "blade-graphics" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e71cfb73b98eb9f58ee84048aa1bdf4e7497fd20c141b57523499fa066b48fed" +dependencies = [ + "ash", + "ash-window", + "bitflags 2.11.1", + "bytemuck", + "codespan-reporting", + "glow", + "gpu-alloc", + "gpu-alloc-ash", + "hidden-trait", + "js-sys", + "khronos-egl", + "libloading", + "log", + "mint", + "naga", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "objc2-ui-kit", + "once_cell", + "raw-window-handle", + "slab", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "blade-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "blade-util" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6be3a82c001ba7a17b6f8e413ede5d1004e6047213f8efaf0ffc15b5c4904c" +dependencies = [ + "blade-graphics", + "bytemuck", + "log", + "profiling", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.117", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[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 = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.11.1", + "block", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "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.11.1", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "command-fds" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f" +dependencies = [ + "nix 0.31.2", + "thiserror 2.0.18", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "deflate64", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" +dependencies = [ + "bitflags 2.11.1", + "block", + "cfg-if", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef" +dependencies = [ + "block", + "core-foundation 0.10.0", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" +dependencies = [ + "bitflags 2.11.1", + "fontdb 0.16.2", + "log", + "rangemap", + "rustc-hash 1.1.0", + "rustybuzz 0.14.1", + "self_cell", + "smol_str", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "data-encoding-macro" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +dependencies = [ + "data-encoding", + "syn 2.0.117", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtor" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "ente-binfmt-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ente-brain" +version = "0.0.1" +dependencies = [ + "anyhow", + "base64", + "bincode", + "ente-card", + "ente-cas", + "postcard", + "serde", + "serde_json", + "tokio", + "tracing", + "ulid", +] + +[[package]] +name = "ente-bus" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-card", + "ente-echo", + "postcard", + "serde", + "tokio", + "tracing", + "ulid", +] + +[[package]] +name = "ente-card" +version = "0.0.1" +dependencies = [ + "serde", + "serde_json", + "ulid", +] + +[[package]] +name = "ente-cas" +version = "0.0.1" +dependencies = [ + "anyhow", + "sha2", + "tracing", +] + +[[package]] +name = "ente-echo" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ente-hostnamed-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "libc", + "nix 0.29.0", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-journald-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "ente-cas", + "libc", + "nix 0.29.0", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ente-kernel" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-card", + "libc", + "nix 0.29.0", + "tokio", + "tracing", +] + +[[package]] +name = "ente-localed-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-logind-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-machined-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-notify-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "libc", + "nix 0.29.0", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ente-policy-provider" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ente-polkit-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-resolved-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "libc", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-snapshot" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-card", + "serde", + "serde_json", + "ulid", +] + +[[package]] +name = "ente-soma" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "libc", + "nix 0.29.0", + "tracing", +] + +[[package]] +name = "ente-systemd1-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-timedated-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus 4.4.0", +] + +[[package]] +name = "ente-timer-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "ulid", +] + +[[package]] +name = "ente-tmpfiles-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "libc", + "nix 0.29.0", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ente-wasm" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-card", + "tracing", + "ulid", + "wasmi", + "wat", +] + +[[package]] +name = "ente-zero" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-brain", + "ente-bus", + "ente-card", + "ente-cas", + "ente-echo", + "ente-kernel", + "ente-snapshot", + "ente-soma", + "ente-wasm", + "libc", + "nix 0.29.0", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "ulid", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.25.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-bounded" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" +dependencies = [ + "futures-timer", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.4.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-ash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a" +dependencies = [ + "ash", + "gpu-alloc-types", + "tinyvec", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "gpui" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "979b45cfa6ec723b6f42330915a1b3769b930d02b2d505f9697f8ca602bee707" +dependencies = [ + "anyhow", + "as-raw-xcb-connection", + "ashpd 0.11.1", + "async-task", + "bindgen", + "blade-graphics", + "blade-macros", + "blade-util", + "block", + "bytemuck", + "calloop", + "calloop-wayland-source", + "cbindgen", + "cocoa 0.26.0", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "core-video", + "cosmic-text", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "filedescriptor", + "flume", + "foreign-types", + "futures", + "gpui-macros", + "gpui_collections", + "gpui_http_client", + "gpui_media", + "gpui_refineable", + "gpui_semantic_version", + "gpui_sum_tree", + "gpui_util", + "gpui_util_macros", + "image", + "inventory", + "itertools 0.14.0", + "libc", + "log", + "lyon", + "metal", + "naga", + "num_cpus", + "objc", + "oo7", + "open", + "parking", + "parking_lot 0.12.5", + "pathfinder_geometry", + "pin-project", + "postage", + "profiling", + "rand 0.9.4", + "raw-window-handle", + "resvg", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "smol", + "stacksafe", + "strum 0.27.2", + "taffy", + "thiserror 2.0.18", + "usvg", + "uuid", + "waker-fn", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols 0.31.2", + "wayland-protocols-plasma", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-numerics 0.2.0", + "windows-registry 0.5.3", + "x11-clipboard", + "x11rb", + "xkbcommon", + "zed-font-kit", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb02dd63a2859714ac7b6b476937617c3c744157af1b49f7c904023a79039be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gpui_collections" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae39dc6d3d201be97e4bc08d96dbef2bc5b5c3d5734e05786e8cc3043342351c" +dependencies = [ + "indexmap", + "rustc-hash 2.1.2", +] + +[[package]] +name = "gpui_derive_refineable" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644de174341a87b3478bd65b66bca38af868bcf2b2e865700523734f83cfc664" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gpui_http_client" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23822b0a6d2c5e6a42507980a0ab3848610ea908942c8ef98187f646f690335e" +dependencies = [ + "anyhow", + "async-compression", + "async-fs", + "bytes", + "derive_more", + "futures", + "gpui_util", + "http", + "http-body", + "log", + "parking_lot 0.12.5", + "serde", + "serde_json", + "sha2", + "tempfile", + "url", + "zed-async-tar", + "zed-reqwest", +] + +[[package]] +name = "gpui_media" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cb8912ae17371725132d2b7eec6797a255accc95d58ee5c1134b529810f14b" +dependencies = [ + "anyhow", + "bindgen", + "core-foundation 0.10.0", + "core-video", + "ctor", + "foreign-types", + "metal", + "objc", +] + +[[package]] +name = "gpui_perf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40a0961dcf598955130e867f4b731150a20546427b41b1a63767c1037a86d77" +dependencies = [ + "gpui_collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui_refineable" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258cb099254e9468181aee5614410fba61db4ae115fc1d51b4a0b985f60d6641" +dependencies = [ + "gpui_derive_refineable", +] + +[[package]] +name = "gpui_semantic_version" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201e45eff7b695528fb3af6560a534943fbc2db5323d755b9d198bd743948e35" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "gpui_sum_tree" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f3bedd573fafafa13d1200b356c588cf094fb2786e3684bb3f5ea59b549fa9" +dependencies = [ + "arrayvec", + "log", + "rayon", +] + +[[package]] +name = "gpui_util" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68faea25903ae524de9af83990b9aa51bcbc8dd085929ac0aea7fd41905e05c3" +dependencies = [ + "anyhow", + "async-fs", + "async_zip", + "command-fds", + "dirs 4.0.0", + "dunce", + "futures", + "futures-lite 1.13.0", + "globset", + "gpui_collections", + "itertools 0.14.0", + "libc", + "log", + "nix 0.29.0", + "regex", + "rust-embed", + "schemars", + "serde", + "serde_json", + "serde_json_lenient", + "shlex", + "smol", + "take-until", + "tempfile", + "tendril", + "unicase", + "walkdir", + "which", +] + +[[package]] +name = "gpui_util_macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c28f65ef47fb97e21e82fd4dd75ccc2506eda010c846dc8054015ea234f1a22" +dependencies = [ + "gpui_perf", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "grid" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "socket2 0.5.10", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot 0.12.5", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hidden-trait" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed9e850438ac849bec07e7d09fbe9309cbd396a5988c30b010580ce08860df" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if-addrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "if-watch" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" +dependencies = [ + "async-io", + "core-foundation 0.9.4", + "fnv", + "futures", + "if-addrs", + "ipnet", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "rtnetlink", + "system-configuration 0.7.0", + "tokio", + "windows 0.62.2", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.4", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.0", + "core-foundation-sys", + "leaky-cow", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.3", + "widestring", + "windows-registry 0.6.1", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" +dependencies = [ + "bitflags 2.11.1", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libp2p" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" +dependencies = [ + "bytes", + "either", + "futures", + "futures-timer", + "getrandom 0.2.17", + "libp2p-allow-block-list", + "libp2p-connection-limits", + "libp2p-core", + "libp2p-dns", + "libp2p-identify", + "libp2p-identity", + "libp2p-kad", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-noise", + "libp2p-quic", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-upnp", + "libp2p-yamux", + "multiaddr", + "pin-project", + "rw-stream-sink", + "thiserror 2.0.18", +] + +[[package]] +name = "libp2p-allow-block-list" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-connection-limits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-core" +version = "0.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select", + "parking_lot 0.12.5", + "pin-project", + "quick-protobuf", + "rand 0.8.6", + "rw-stream-sink", + "thiserror 2.0.18", + "tracing", + "unsigned-varint 0.8.0", + "web-time", +] + +[[package]] +name = "libp2p-dns" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" +dependencies = [ + "async-trait", + "futures", + "hickory-resolver", + "libp2p-core", + "libp2p-identity", + "parking_lot 0.12.5", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-identify" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "libp2p-identity" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" +dependencies = [ + "bs58", + "ed25519-dalek", + "hkdf", + "multihash", + "quick-protobuf", + "rand 0.8.6", + "sha2", + "thiserror 2.0.18", + "tracing", + "zeroize", +] + +[[package]] +name = "libp2p-kad" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d3fd632a5872ec804d37e7413ceea20588f69d027a0fa3c46f82574f4dee60" +dependencies = [ + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.6", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tracing", + "uint", + "web-time", +] + +[[package]] +name = "libp2p-mdns" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" +dependencies = [ + "futures", + "hickory-proto", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "smallvec", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-metrics" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-identify", + "libp2p-identity", + "libp2p-kad", + "libp2p-swarm", + "pin-project", + "prometheus-client", + "web-time", +] + +[[package]] +name = "libp2p-noise" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core", + "libp2p-identity", + "multiaddr", + "multihash", + "quick-protobuf", + "rand 0.8.6", + "snow", + "static_assertions", + "thiserror 2.0.18", + "tracing", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "libp2p-quic" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-tls", + "quinn", + "rand 0.8.6", + "ring", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-stream" +version = "0.4.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6bd8025c80205ec2810cfb28b02f362ab48a01bee32c50ab5f12761e033464" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "tracing", +] + +[[package]] +name = "libp2p-swarm" +version = "0.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce88c6c4bf746c8482480345ea3edfd08301f49e026889d1cbccfa1808a9ed9e" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "hashlink 0.10.0", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm-derive", + "multistream-select", + "rand 0.8.6", + "smallvec", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" +dependencies = [ + "heck 0.5.0", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "libp2p-tcp" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core", + "socket2 0.6.3", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-tls" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core", + "libp2p-identity", + "rcgen", + "ring", + "rustls", + "rustls-webpki", + "thiserror 2.0.18", + "x509-parser", + "yasna", +] + +[[package]] +name = "libp2p-upnp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" +dependencies = [ + "futures", + "futures-timer", + "igd-next", + "libp2p-core", + "libp2p-swarm", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-yamux" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" +dependencies = [ + "either", + "futures", + "libp2p-core", + "thiserror 2.0.18", + "tracing", + "yamux 0.12.1", + "yamux 0.13.10", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.5", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8575c0d003ae459399623c4def180c63b77f343b1a7fee64f249b349e7699a31" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-graphics-types 0.1.3", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minga-cli" +version = "0.1.0" +dependencies = [ + "clap", + "futures", + "libp2p", + "minga-core", + "minga-p2p", + "minga-store", + "notify", + "rpassword", + "tempfile", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "minga-core" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "argon2", + "blake3", + "ed25519-dalek", + "postcard", + "rand 0.8.6", + "serde", + "serde-big-array", + "thiserror 2.0.18", + "tree-sitter", + "tree-sitter-rust", +] + +[[package]] +name = "minga-p2p" +version = "0.1.0" +dependencies = [ + "futures", + "libp2p", + "libp2p-stream", + "minga-core", + "minga-store", + "postcard", + "rand 0.8.6", + "serde", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", +] + +[[package]] +name = "minga-store" +version = "0.1.0" +dependencies = [ + "blake3", + "minga-core", + "postcard", + "serde", + "sled", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "minga-vfs" +version = "0.1.0" +dependencies = [ + "minga-core", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot 0.12.5", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multi-stash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" + +[[package]] +name = "multiaddr" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "libp2p-identity", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint 0.8.0", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" +dependencies = [ + "unsigned-varint 0.8.0", +] + +[[package]] +name = "multistream-select" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint 0.7.2", +] + +[[package]] +name = "naga" +version = "25.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.1", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.15.5", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "strum 0.26.3", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags 2.11.1", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oo7" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d" +dependencies = [ + "aes", + "ashpd 0.12.3", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite 2.6.1", + "futures-util", + "getrandom 0.3.4", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "rand 0.9.4", + "serde", + "sha2", + "subtle", + "zbus 5.15.0", + "zbus_macros 5.15.0", + "zeroize", + "zvariant 5.11.0", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "5.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.4.1", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot 0.12.5", + "pin-project", + "pollster", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +dependencies = [ + "dtoa", + "itoa", + "parking_lot 0.12.5", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror 1.0.69", + "unsigned-varint 0.8.0", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.2", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" +dependencies = [ + "cocoa 0.25.0", + "core-graphics 0.23.2", + "objc", + "raw-window-handle", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rpassword" +version = "7.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + +[[package]] +name = "rtnetlink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "nix 0.30.1", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.11.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.9.1", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.117", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring 0.2.0", + "unicode-ccc 0.2.0", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser 0.25.1", + "unicode-bidi-mirroring 0.4.0", + "unicode-ccc 0.4.0", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_json_lenient" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" + +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "ring", + "rustc_version", + "sha2", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "stacksafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090" +dependencies = [ + "stacker", + "stacksafe-macro", +] + +[[package]] +name = "stacksafe-macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" +dependencies = [ + "proc-macro-error2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "string-interner" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3275464d7a9f2d4cac57c89c2ef96a8524dba2864c8d6f82e3980baf136f9b" +dependencies = [ + "hashbrown 0.15.5", + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sval" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05195a68cb8336336413c3f52ea0467c7666f5f652e01ba807319ffcc090f2" + +[[package]] +name = "sval_buffer" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2887fd5e2454319f2f170860427f615451bb057fc9b3fd7f28b7a99ac2ccf0e4" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ddf3833aa407554ad40be081aea9cc3ea6d6798832ec83dd35022b45373896" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94398bdd2ee99eee108e0b7b0df9081700a96cdefac3add5c09ad3e6f2376bcb" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f51ae40f37b541fdf81531e51d5071e8edc7c5a191cc23d288b654995b9eaba" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abe05be8455348e3f6edc14a85fd2fcf32f801a14f0126ce025e63851b4f13b" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36361fa3c42c9fd129f4c5023a812d5b275742b28a672c758ec70049c61df00" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb76707179eccecab345902a803aacacab6889f3af675dbe89766bc790b1a6cb" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take-until" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand 2.4.1", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +dependencies = [ + "bytes", + "libc", + "mio 1.2.0", + "parking_lot 0.12.5", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tree-sitter" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-rust" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.4", + "serde", + "web-time", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb 0.23.0", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz 0.20.1", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6" +dependencies = [ + "leb128fmt", + "wasmparser 0.248.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmi" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" +dependencies = [ + "arrayvec", + "multi-stash", + "smallvec", + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasmi_collections" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" +dependencies = [ + "string-interner", +] + +[[package]] +name = "wasmi_core" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" +dependencies = [ + "downcast-rs", + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" +dependencies = [ + "bitflags 2.11.1", + "indexmap", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" +dependencies = [ + "bitflags 2.11.1", + "indexmap", + "semver", +] + +[[package]] +name = "wast" +version = "248.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc54622ed5a5cddafcdf152043f9d4aed54d4a653d686b7dfe874809fca99d7" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.248.0", +] + +[[package]] +name = "wat" +version = "1.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75cd9e510603909748e6ebab89f27cd04472c1d9d85a3c88a7a6fc51a1a7934" +dependencies = [ + "wast", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.3", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[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 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot 0.12.5", + "rayon", + "thiserror 2.0.18", + "windows 0.61.3", + "windows-future 0.2.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]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "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]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[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]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "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 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix 1.1.4", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "xcb" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yahweh-bus" +version = "0.1.0" +dependencies = [ + "gpui", +] + +[[package]] +name = "yahweh-core" +version = "0.1.0" +dependencies = [ + "async-trait", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "yahweh-database-explorer" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-core", + "yahweh-provider-sqlite", + "yahweh-theme", + "yahweh-widget-tree", +] + +[[package]] +name = "yahweh-file-explorer" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-core", + "yahweh-provider-fs", + "yahweh-theme", + "yahweh-widget-text-input", + "yahweh-widget-tree", +] + +[[package]] +name = "yahweh-image-viewer" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-bus", + "yahweh-theme", +] + +[[package]] +name = "yahweh-provider-fs" +version = "0.1.0" +dependencies = [ + "async-trait", + "notify", + "tokio", + "yahweh-core", +] + +[[package]] +name = "yahweh-provider-sqlite" +version = "0.1.0" +dependencies = [ + "async-trait", + "rusqlite", + "tokio", + "yahweh-core", +] + +[[package]] +name = "yahweh-shell" +version = "0.1.0" +dependencies = [ + "gpui", + "notify", + "serde", + "serde_json", + "tokio", + "yahweh-bus", + "yahweh-core", + "yahweh-database-explorer", + "yahweh-file-explorer", + "yahweh-image-viewer", + "yahweh-provider-fs", + "yahweh-provider-sqlite", + "yahweh-text-viewer", + "yahweh-theme", + "yahweh-widget-container-core", + "yahweh-widget-splitter", + "yahweh-widget-tabs", + "yahweh-widget-tiled", + "yahweh-widget-tree", +] + +[[package]] +name = "yahweh-text-viewer" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-bus", + "yahweh-core", + "yahweh-provider-fs", + "yahweh-provider-sqlite", + "yahweh-theme", +] + +[[package]] +name = "yahweh-theme" +version = "0.1.0" +dependencies = [ + "gpui", +] + +[[package]] +name = "yahweh-widget-container-core" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-core", +] + +[[package]] +name = "yahweh-widget-splitter" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-core", + "yahweh-theme", + "yahweh-widget-container-core", +] + +[[package]] +name = "yahweh-widget-tabs" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-core", + "yahweh-theme", + "yahweh-widget-container-core", +] + +[[package]] +name = "yahweh-widget-text-input" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-theme", +] + +[[package]] +name = "yahweh-widget-tiled" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-core", + "yahweh-theme", + "yahweh-widget-container-core", +] + +[[package]] +name = "yahweh-widget-tree" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-theme", +] + +[[package]] +name = "yamux" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot 0.12.5", + "pin-project", + "rand 0.8.6", + "static_assertions", +] + +[[package]] +name = "yamux" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1991f6690292030e31b0144d73f5e8368936c58e45e7068254f7138b23b00672" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot 0.12.5", + "pin-project", + "rand 0.9.4", + "static_assertions", + "web-time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.6", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-lite 2.6.1", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.2", + "zbus_macros 5.15.0", + "zbus_names 4.3.2", + "zvariant 5.11.0", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names 4.3.2", + "zvariant 5.11.0", + "zvariant_utils 3.3.1", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.2", + "zvariant 5.11.0", +] + +[[package]] +name = "zed-async-tar" +version = "0.5.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf4b5f655e29700e473cb1acd914ab112b37b62f96f7e642d5fc6a0c02eb881" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall 0.2.16", + "xattr", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.11.1", + "byteorder", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-reqwest" +version = "0.12.15-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2d05756ff48539950c3282ad7acf3817ad3f08797c205ad1c34a2ce03b9970" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration 0.6.1", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.6", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 1.0.2", + "zvariant_derive 5.11.0", + "zvariant_utils 3.3.1", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 3.3.1", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.2", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a8ca317 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,175 @@ +[workspace] +resolver = "2" +members = [ + # ============================================================ + # core/ — Init y compat (arje absorbido) + # ============================================================ + "crates/core/ente-card", + "crates/core/ente-bus", + "crates/core/ente-cas", + "crates/core/ente-kernel", + "crates/core/ente-soma", + "crates/core/ente-wasm", + "crates/core/ente-snapshot", + "crates/core/ente-brain", + "crates/core/ente-zero", + "crates/core/ente-echo", + "crates/core/ente-policy-provider", + "crates/core/ente-logind-compat", + "crates/core/ente-hostnamed-compat", + "crates/core/ente-timedated-compat", + "crates/core/ente-localed-compat", + "crates/core/ente-journald-compat", + "crates/core/ente-resolved-compat", + "crates/core/ente-polkit-compat", + "crates/core/ente-machined-compat", + "crates/core/ente-tmpfiles-compat", + "crates/core/ente-systemd1-compat", + "crates/core/ente-notify-compat", + "crates/core/ente-binfmt-compat", + "crates/core/ente-timer-compat", + + # ============================================================ + # modules/semantic_dht/ — DHT semántico (minga absorbido) + # ============================================================ + "crates/modules/semantic_dht/minga-core", + "crates/modules/semantic_dht/minga-store", + "crates/modules/semantic_dht/minga-p2p", + "crates/modules/semantic_dht/minga-vfs", + "crates/modules/semantic_dht/minga-cli", + + # ============================================================ + # modules/ui_engine/ — Motor de widgets (yahweh libs+widgets) + # ============================================================ + "crates/modules/ui_engine/libs/core", + "crates/modules/ui_engine/libs/theme", + "crates/modules/ui_engine/libs/bus", + "crates/modules/ui_engine/libs/providers/fs", + "crates/modules/ui_engine/libs/providers/sqlite", + "crates/modules/ui_engine/widgets/tree", + "crates/modules/ui_engine/widgets/container_core", + "crates/modules/ui_engine/widgets/splitter", + "crates/modules/ui_engine/widgets/tabs", + "crates/modules/ui_engine/widgets/tiled", + "crates/modules/ui_engine/widgets/text_input", + + # ============================================================ + # apps/ — apps que consumen el protocolo (yahweh modules+shell) + # ============================================================ + "crates/apps/file_explorer", + "crates/apps/database_explorer", + "crates/apps/text_viewer", + "crates/apps/image_viewer", + "crates/apps/yahweh-shell", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +rust-version = "1.80" +license = "MIT OR Apache-2.0" +authors = ["Brahman Contributors"] +publish = false +repository = "https://example.invalid/brahman" + +[workspace.dependencies] +# === Serialización === +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde-big-array = "0.5" +postcard = { version = "1", features = ["use-std"] } +toml = "0.8" +bincode = "1" +base64 = "0.22" + +# === Errores === +thiserror = "2" # bump uniforme; arje (era 1) puede requerir ajustes menores +anyhow = "1" + +# === Async === +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["compat"] } +async-trait = "0.1" +futures = "0.3" + +# === Observabilidad === +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } + +# === Linux primitives (arje) === +nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user"] } +libc = "0.2" + +# === IDs / Hash / Crypto === +ulid = { version = "1", features = ["serde"] } +uuid = { version = "1", features = ["v4"] } +sha2 = "0.10" +blake3 = "1.5" +ed25519-dalek = "2" +aes-gcm = "0.10" +argon2 = "0.5" +rand = "0.8" + +# === WASM (arje) === +wasmi = "0.40" +wat = "1" + +# === Storage / DB === +sled = "0.34" +rusqlite = { version = "0.31", features = ["bundled", "blob"] } + +# === P2P (minga) === +libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify"] } +libp2p-stream = "=0.4.0-alpha" + +# === Code parsing (minga) === +tree-sitter = "0.24" +tree-sitter-rust = "0.23" + +# === FS notify === +notify = "6.1" + +# === CLI / auth (minga) === +clap = { version = "4", features = ["derive"] } +rpassword = "7" + +# === D-Bus (arje compat) === +zbus = { version = "4", default-features = false, features = ["tokio"] } + +# === Tests === +tempfile = "3" + +# === GPUI (yahweh) === +gpui = "0.2" + +# === Filesystem helpers === +directories = "5" + +# ============================================================ +# Intra-workspace deps de yahweh (referenciadas por workspace = true) +# ============================================================ +yahweh-core = { path = "crates/modules/ui_engine/libs/core" } +yahweh-theme = { path = "crates/modules/ui_engine/libs/theme" } +yahweh-bus = { path = "crates/modules/ui_engine/libs/bus" } +yahweh-provider-fs = { path = "crates/modules/ui_engine/libs/providers/fs" } +yahweh-provider-sqlite = { path = "crates/modules/ui_engine/libs/providers/sqlite" } +yahweh-widget-tree = { path = "crates/modules/ui_engine/widgets/tree" } +yahweh-widget-container-core = { path = "crates/modules/ui_engine/widgets/container_core" } +yahweh-widget-splitter = { path = "crates/modules/ui_engine/widgets/splitter" } +yahweh-widget-tabs = { path = "crates/modules/ui_engine/widgets/tabs" } +yahweh-widget-tiled = { path = "crates/modules/ui_engine/widgets/tiled" } +yahweh-widget-text-input = { path = "crates/modules/ui_engine/widgets/text_input" } +yahweh-file-explorer = { path = "crates/apps/file_explorer" } +yahweh-database-explorer = { path = "crates/apps/database_explorer" } +yahweh-text-viewer = { path = "crates/apps/text_viewer" } +yahweh-image-viewer = { path = "crates/apps/image_viewer" } + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = "symbols" +panic = "abort" + +[profile.dev] +opt-level = 0 +debug = true diff --git a/crates/apps/database_explorer/Cargo.toml b/crates/apps/database_explorer/Cargo.toml new file mode 100644 index 0000000..08bd932 --- /dev/null +++ b/crates/apps/database_explorer/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "yahweh-database-explorer" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +description = "Explorer de SQLite — composición TreeView + SqliteProvider con lazy load." + +[dependencies] +gpui = { workspace = true } +yahweh-core = { workspace = true } +yahweh-theme = { workspace = true } +yahweh-widget-tree = { workspace = true } +yahweh-provider-sqlite = { workspace = true } diff --git a/crates/apps/database_explorer/src/lib.rs b/crates/apps/database_explorer/src/lib.rs new file mode 100644 index 0000000..49782da --- /dev/null +++ b/crates/apps/database_explorer/src/lib.rs @@ -0,0 +1,284 @@ +//! `yahweh_database_explorer` — explorer de SQLite. +//! +//! Mismo patrón que `yahweh_file_explorer` pero con `SqliteProvider`. La +//! UX es idéntica (TreeView con lazy load por chevron); cambia solo el +//! origen de los datos: filas de una tabla `items(id, parent_id, name, +//! display_type, content)` en lugar del filesystem. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use gpui::{ + Context, Entity, EventEmitter, IntoElement, Render, SharedString, Window, div, prelude::*, + px, +}; + +use yahweh_core::{DataProvider, DisplayType, EntityNode}; +use yahweh_provider_sqlite::SqliteDataProvider; +use yahweh_theme::Theme; +use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView}; + +#[derive(Clone, Debug)] +#[allow(dead_code)] // Consumido por el AppBus en Fase 4+. +pub enum DatabaseExplorerEvent { + EntitySelected { id: String }, + EntityOpened { id: String }, +} + +pub struct DatabaseExplorer { + tree_view: Entity, + provider: Arc, + db_path: String, + + expanded: HashSet, + children: HashMap>, + pending: HashSet, + /// Mensaje de error si la DB no abrió. Se muestra en el header. + open_error: Option, +} + +const ROOT_KEY: &str = "__db_root__"; + +impl EventEmitter for DatabaseExplorer {} + +impl DatabaseExplorer { + /// `db_path` es la ruta al .sqlite. Si no existe se crea con la tabla + /// `items` y un seed mínimo (ver `SqliteDataProvider::new`). + pub fn new(db_path: String, cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()).detach(); + + let tree_view = cx.new(|cx| TreeView::new("db-explorer-tree", cx)); + cx.subscribe(&tree_view, |this: &mut DatabaseExplorer, _, ev, cx| { + this.on_tree_event(ev, cx); + }) + .detach(); + + let (provider, open_error) = match SqliteDataProvider::new(&db_path) { + Ok(p) => (Some(Arc::new(p)), None), + Err(e) => (None, Some(e)), + }; + + let mut expanded = HashSet::new(); + expanded.insert(ROOT_KEY.to_string()); + + let mut me = Self { + tree_view, + // Usamos un dummy provider si la DB no abrió. La UI mostrará el + // error en el header; cualquier load_children retornará vacío. + provider: provider.unwrap_or_else(|| { + Arc::new(SqliteDataProvider::new(":memory:").expect("memory db")) + }), + db_path, + expanded, + children: HashMap::new(), + pending: HashSet::new(), + open_error, + }; + // Cargar el root (parent_id NULL en SQLite, mapped to None acá). + me.load_children(ROOT_KEY.to_string(), cx); + me + } + + pub fn db_path(&self) -> &str { + &self.db_path + } + + fn load_children(&mut self, parent_key: String, cx: &mut Context) { + if self.pending.contains(&parent_key) || self.children.contains_key(&parent_key) { + return; + } + self.pending.insert(parent_key.clone()); + + let provider = self.provider.clone(); + let parent_for_task = parent_key.clone(); + cx.spawn(async move |this, cx| { + // ROOT_KEY → None; cualquier otro → Some(actual id). + let arg: Option = if parent_for_task == ROOT_KEY { + None + } else { + Some(parent_for_task.clone()) + }; + let result = provider.list_children(arg.as_deref()).await; + let _ = this.update(cx, |this, cx| { + this.on_children_loaded(parent_for_task, result, cx); + }); + }) + .detach(); + } + + fn on_children_loaded( + &mut self, + parent_key: String, + result: Result, String>, + cx: &mut Context, + ) { + self.pending.remove(&parent_key); + let mut entries = result.unwrap_or_default(); + sort_entries(&mut entries); + self.children.insert(parent_key, entries); + self.push_rows(cx); + } + + fn push_rows(&self, cx: &mut Context) { + let mut rows = Vec::new(); + // Una row "raíz virtual" para que el árbol tenga un anchor visible. + rows.push(TreeRow { + id: RowId::new(ROOT_KEY), + label: format!("(db) {}", self.db_path), + depth: 0, + kind: RowKind::Branch, + expanded: self.expanded.contains(ROOT_KEY), + icon: Some("🗄️".to_string()), + }); + if self.expanded.contains(ROOT_KEY) { + self.append_children(ROOT_KEY, 1, &mut rows); + } + + self.tree_view + .update(&mut *cx, |tree, cx| tree.set_rows(rows, cx)); + } + + fn append_children(&self, parent: &str, depth: u32, out: &mut Vec) { + let Some(children) = self.children.get(parent) else { return }; + for entry in children { + let kind = match entry.display_type { + DisplayType::Folder => RowKind::Branch, + _ => RowKind::Leaf, + }; + let icon = match entry.display_type { + DisplayType::Folder => "📂", + DisplayType::File => "📄", + DisplayType::Stream => "📡", + }; + let is_expanded = self.expanded.contains(&entry.id); + out.push(TreeRow { + id: RowId::new(entry.id.clone()), + label: entry.name.clone(), + depth, + kind, + expanded: is_expanded, + icon: Some(icon.to_string()), + }); + if is_expanded { + self.append_children(&entry.id, depth + 1, out); + } + } + } + + fn on_tree_event(&mut self, event: &TreeEvent, cx: &mut Context) { + match event { + TreeEvent::ChevronToggled(id) => { + let key = id.as_str().to_string(); + if !self.expanded.remove(&key) { + self.expanded.insert(key.clone()); + self.load_children(key, cx); + } + self.push_rows(cx); + } + TreeEvent::RowClicked(id) => { + let key = id.as_str(); + if key == ROOT_KEY { + return; + } + if let Some(entry) = self.find_entry(key) { + if !matches!(entry.display_type, DisplayType::Folder) { + cx.emit(DatabaseExplorerEvent::EntitySelected { + id: key.to_string(), + }); + } + } + } + TreeEvent::RowDoubleClicked(id) => { + let key = id.as_str(); + if key == ROOT_KEY { + return; + } + if let Some(entry) = self.find_entry(key) { + if !matches!(entry.display_type, DisplayType::Folder) { + cx.emit(DatabaseExplorerEvent::EntityOpened { + id: key.to_string(), + }); + } + } + } + TreeEvent::ContextMenuRequested { .. } | TreeEvent::ActiveChanged(_) => {} + } + } + + fn find_entry(&self, id: &str) -> Option<&EntityNode> { + for entries in self.children.values() { + if let Some(e) = entries.iter().find(|e| e.id == id) { + return Some(e); + } + } + None + } +} + +impl Render for DatabaseExplorer { + fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = Theme::global(cx).clone(); + let pending_count = self.pending.len(); + + div() + .size_full() + .bg(theme.bg_panel.clone()) + .flex() + .flex_col() + .child( + div() + .h(px(28.0)) + .px(px(8.0)) + .border_b_1() + .border_color(theme.border) + .flex() + .flex_row() + .items_center() + .gap(px(6.0)) + .child( + div() + .text_size(px(11.0)) + .text_color(theme.fg_muted) + .child("🗄️"), + ) + .child( + div() + .text_size(px(11.0)) + .text_color(theme.fg_text) + .child(SharedString::from(self.db_path.clone())), + ) + .child( + div() + .ml_auto() + .text_size(px(10.0)) + .text_color(theme.fg_muted) + .child(SharedString::from(if pending_count > 0 { + format!("⏳ {}", pending_count) + } else { + String::new() + })), + ), + ) + .child(if let Some(err) = self.open_error.clone() { + // Si la DB no abrió, mostramos el error y no pintamos el + // árbol vacío — sería confuso. + div() + .p(px(12.0)) + .text_size(px(11.0)) + .text_color(theme.accent_strong) + .child(SharedString::from(format!("error abriendo DB: {}", err))) + } else { + div().flex_grow().min_h(px(0.0)).child(self.tree_view.clone()) + }) + } +} + +fn sort_entries(entries: &mut Vec) { + entries.sort_by(|a, b| { + let a_dir = matches!(a.display_type, DisplayType::Folder); + let b_dir = matches!(b.display_type, DisplayType::Folder); + b_dir + .cmp(&a_dir) + .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) + }); +} diff --git a/crates/apps/file_explorer/Cargo.toml b/crates/apps/file_explorer/Cargo.toml new file mode 100644 index 0000000..0835f69 --- /dev/null +++ b/crates/apps/file_explorer/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "yahweh-file-explorer" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +description = "Explorer de filesystem — composición TreeView + FsProvider con lazy load." + +[dependencies] +gpui = { workspace = true } +yahweh-core = { workspace = true } +yahweh-theme = { workspace = true } +yahweh-widget-tree = { workspace = true } +yahweh-widget-text-input = { workspace = true } +yahweh-provider-fs = { workspace = true } diff --git a/crates/apps/file_explorer/src/lib.rs b/crates/apps/file_explorer/src/lib.rs new file mode 100644 index 0000000..4b96cc3 --- /dev/null +++ b/crates/apps/file_explorer/src/lib.rs @@ -0,0 +1,786 @@ +//! `yahweh_file_explorer` — explorer de filesystem con menú contextual. +//! +//! Composición canónica del patrón "explorer = TreeView + provider": +//! +//! ```text +//! FileExplorer +//! ├── TreeView (widgets/tree, agnóstico) +//! └── FsProvider (libs/providers/fs, async + tokio::io) +//! ``` +//! +//! Estado: +//! - `expanded`: set de paths cuyo chevron está abierto. +//! - `children`: cache de hijos por path parent (cargado lazy via +//! `cx.spawn` + provider). +//! - `pending`: set de loads en vuelo (anti re-trigger). +//! - `menu`: estado del menú contextual flotante (Fase 4.5). +//! +//! Menú contextual (Fase 4.5): +//! Right-click sobre una fila o el área vacía del árbol abre un menú con +//! las acciones FS habituales. Las mutaciones se ejecutan sincrónicamente +//! contra `std::fs` y luego se invalida el cache del directorio afectado +//! para forzar reload del provider. +//! +//! Acciones soportadas: +//! - **Open** (solo file): emite `FileExplorerEvent::FileOpened` (que el +//! forwarder de la Shell traduce a `AppEvent::EntityOpened` al bus). +//! - **Copy path**: copia el path absoluto al clipboard. +//! - **New file**: crea un archivo vacío con nombre auto-generado +//! (`new_file_NN.txt`) en el directorio elegido — sin necesitar text +//! input por ahora; renombrar viene cuando sumemos un TextInput +//! widget. +//! - **New folder**: crea un directorio con nombre auto (`new_folder_NN`). +//! - **Delete**: confirmación vía `window.prompt` (dialog del platform); +//! si el usuario confirma, `remove_file` o `remove_dir_all` según +//! corresponda. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use gpui::{ + ClickEvent, ClipboardItem, Context, Entity, EventEmitter, IntoElement, Pixels, Point, + PromptLevel, Render, SharedString, Window, div, prelude::*, px, +}; + +use yahweh_core::{DataProvider, DisplayType, EntityNode}; +use yahweh_provider_fs::FileDataProvider; +use yahweh_theme::Theme; +use yahweh_widget_text_input::{TextInput, TextInputEvent}; +use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView}; + +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub enum FileExplorerEvent { + FileSelected { path: String }, + FileOpened { path: String }, + RootChanged { path: String }, +} + +#[derive(Clone, Debug)] +struct MenuState { + /// `None` ⇒ menú "fondo" (área vacía del tree). `Some` ⇒ sobre una + /// fila concreta. + target: Option, + /// Posición absoluta donde se hizo el right-click. Para el overlay + /// usamos coords window-relative (gpui las maneja como px). + position: Point, +} + +#[derive(Clone, Debug)] +struct MenuTarget { + id: String, + is_folder: bool, +} + +pub struct FileExplorer { + tree_view: Entity, + provider: Arc, + + root: String, + expanded: HashSet, + children: HashMap>, + pending: HashSet, + menu: Option, + /// Modal de rename activo. `None` ⇒ no hay modal. + rename: Option, +} + +/// Estado del modal de rename. El TextInput vive como sub-entity para +/// que pueda recibir focus + key events. El target_path lo lleva la +/// closure de subscripción (no lo necesitamos en `self` aparte). +#[derive(Clone)] +struct RenameState { + original_name: String, + input: Entity, +} + +impl EventEmitter for FileExplorer {} + +impl FileExplorer { + pub fn new(root: String, cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()).detach(); + + let tree_view = cx.new(|cx| TreeView::new("file-explorer-tree", cx)); + cx.subscribe(&tree_view, |this: &mut FileExplorer, _, ev, cx| { + this.on_tree_event(ev, cx); + }) + .detach(); + + let mut expanded = HashSet::new(); + expanded.insert(root.clone()); + + let mut me = Self { + tree_view, + provider: Arc::new(FileDataProvider), + root: root.clone(), + expanded, + children: HashMap::new(), + pending: HashSet::new(), + menu: None, + rename: None, + }; + me.load_children(root, cx); + me + } + + pub fn root(&self) -> &str { + &self.root + } + + #[allow(dead_code)] + pub fn set_root(&mut self, path: String, cx: &mut Context) { + if path != self.root { + self.root = path.clone(); + self.expanded.insert(path.clone()); + self.load_children(path.clone(), cx); + self.push_rows(cx); + cx.emit(FileExplorerEvent::RootChanged { path }); + } + } + + // ----- load + cache ----- + + fn load_children(&mut self, parent: String, cx: &mut Context) { + if self.pending.contains(&parent) || self.children.contains_key(&parent) { + return; + } + self.pending.insert(parent.clone()); + + let provider = self.provider.clone(); + let parent_for_task = parent.clone(); + cx.spawn(async move |this, cx| { + let result = provider.list_children(Some(&parent_for_task)).await; + let _ = this.update(cx, |this, cx| { + this.on_children_loaded(parent_for_task, result, cx); + }); + }) + .detach(); + } + + /// Versión "force": invalida el cache antes de re-pedir. Se usa tras + /// una mutación FS (new/delete) para que la UI refleje el cambio. + fn refresh_dir(&mut self, parent: String, cx: &mut Context) { + self.children.remove(&parent); + self.pending.remove(&parent); + self.load_children(parent, cx); + } + + fn on_children_loaded( + &mut self, + parent: String, + result: Result, String>, + cx: &mut Context, + ) { + self.pending.remove(&parent); + match result { + Ok(mut children) => { + sort_entries(&mut children); + self.children.insert(parent, children); + self.push_rows(cx); + } + Err(_) => { + self.children.insert(parent, Vec::new()); + self.push_rows(cx); + } + } + } + + fn push_rows(&self, cx: &mut Context) { + let mut rows = Vec::new(); + rows.push(TreeRow { + id: RowId::new(self.root.clone()), + label: self.root.clone(), + depth: 0, + kind: RowKind::Branch, + expanded: self.expanded.contains(&self.root), + icon: Some("📂".to_string()), + }); + if self.expanded.contains(&self.root) { + self.append_children(&self.root, 1, &mut rows); + } + + self.tree_view + .update(cx, |tree, cx| tree.set_rows(rows, cx)); + } + + fn append_children(&self, parent: &str, depth: u32, out: &mut Vec) { + let Some(children) = self.children.get(parent) else { return }; + for entry in children { + let kind = match entry.display_type { + DisplayType::Folder => RowKind::Branch, + _ => RowKind::Leaf, + }; + let icon = match entry.display_type { + DisplayType::Folder => "📁", + DisplayType::File => "📄", + DisplayType::Stream => "📡", + }; + let is_expanded = self.expanded.contains(&entry.id); + out.push(TreeRow { + id: RowId::new(entry.id.clone()), + label: entry.name.clone(), + depth, + kind, + expanded: is_expanded, + icon: Some(icon.to_string()), + }); + if is_expanded { + self.append_children(&entry.id, depth + 1, out); + } + } + } + + // ----- TreeView events ----- + + fn on_tree_event(&mut self, event: &TreeEvent, cx: &mut Context) { + match event { + TreeEvent::ChevronToggled(id) => { + let path = id.as_str().to_string(); + if !self.expanded.remove(&path) { + self.expanded.insert(path.clone()); + self.load_children(path, cx); + } + self.push_rows(cx); + } + TreeEvent::RowClicked(id) => { + let path = id.as_str(); + if let Some(entry) = self.find_entry(path) { + if !matches!(entry.display_type, DisplayType::Folder) { + cx.emit(FileExplorerEvent::FileSelected { + path: path.to_string(), + }); + } + } + // Click primario en cualquier lado también cierra el + // menú contextual si estaba abierto. + if self.menu.is_some() { + self.menu = None; + cx.notify(); + } + } + TreeEvent::RowDoubleClicked(id) => { + let path = id.as_str(); + if let Some(entry) = self.find_entry(path) { + if !matches!(entry.display_type, DisplayType::Folder) { + cx.emit(FileExplorerEvent::FileOpened { + path: path.to_string(), + }); + } + } + } + TreeEvent::ContextMenuRequested { id, position } => { + self.open_menu(id.as_ref().map(|i| i.as_str().to_string()), *position, cx); + } + TreeEvent::ActiveChanged(_) => {} + } + } + + fn find_entry(&self, id: &str) -> Option<&EntityNode> { + for entries in self.children.values() { + if let Some(e) = entries.iter().find(|e| e.id == id) { + return Some(e); + } + } + None + } + + fn is_folder_path(&self, id: &str) -> bool { + self.find_entry(id) + .map(|e| matches!(e.display_type, DisplayType::Folder)) + .unwrap_or_else(|| std::path::Path::new(id).is_dir()) + } + + /// Resuelve el directorio donde crear un nuevo archivo/carpeta según + /// el target del menú: si target es folder → adentro; si target es + /// file → su parent; si target es None (fondo) → root. + fn parent_for_new(&self, target: &Option) -> String { + match target { + None => self.root.clone(), + Some(t) if t.is_folder => t.id.clone(), + Some(t) => std::path::Path::new(&t.id) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_else(|| self.root.clone()), + } + } + + // ----- menú: open/close ----- + + fn open_menu( + &mut self, + id: Option, + position: Point, + cx: &mut Context, + ) { + let target = id.map(|id| { + let is_folder = self.is_folder_path(&id); + MenuTarget { id, is_folder } + }); + self.menu = Some(MenuState { target, position }); + cx.notify(); + } + + fn close_menu(&mut self, cx: &mut Context) { + if self.menu.take().is_some() { + cx.notify(); + } + } + + // ----- acciones del menú ----- + + fn action_open(&mut self, target: MenuTarget, cx: &mut Context) { + if !target.is_folder { + cx.emit(FileExplorerEvent::FileOpened { path: target.id }); + } + self.close_menu(cx); + } + + fn action_copy_path( + &mut self, + target: MenuTarget, + _w: &mut Window, + cx: &mut Context, + ) { + cx.write_to_clipboard(ClipboardItem::new_string(target.id)); + self.close_menu(cx); + } + + fn action_new_file( + &mut self, + target: Option, + cx: &mut Context, + ) { + let parent = self.parent_for_new(&target); + if let Some(name) = next_available_name(&parent, "new_file_", ".txt", false) { + let full = std::path::Path::new(&parent).join(&name); + if let Err(e) = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&full) + { + eprintln!("[FileExplorer] new file {:?}: {}", full, e); + } + } + self.refresh_dir(parent, cx); + self.close_menu(cx); + } + + fn action_new_folder( + &mut self, + target: Option, + cx: &mut Context, + ) { + let parent = self.parent_for_new(&target); + if let Some(name) = next_available_name(&parent, "new_folder_", "", true) { + let full = std::path::Path::new(&parent).join(&name); + if let Err(e) = std::fs::create_dir(&full) { + eprintln!("[FileExplorer] new folder {:?}: {}", full, e); + } + } + self.refresh_dir(parent, cx); + self.close_menu(cx); + } + + fn action_rename( + &mut self, + target: MenuTarget, + window: &mut Window, + cx: &mut Context, + ) { + // Nombre actual = file_name del path. Si por alguna razón no + // tiene file_name (path raíz), usamos el path completo. + let original_name = std::path::Path::new(&target.id) + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| target.id.clone()); + + let initial = original_name.clone(); + let input = cx.new(|cx| TextInput::new(initial, cx)); + + // Subscribirse a los eventos del input para confirmar/cancelar. + cx.subscribe(&input, { + let target_path = target.id.clone(); + let original_name = original_name.clone(); + move |this: &mut FileExplorer, _, ev: &TextInputEvent, cx| match ev { + TextInputEvent::Confirmed(new_name) => { + this.commit_rename(&target_path, &original_name, new_name.clone(), cx); + } + TextInputEvent::Cancelled => { + this.close_rename(cx); + } + } + }) + .detach(); + + // Pedir focus para que las próximas teclas vayan al input. Hay + // que hacerlo después de que el render lo monte; lo más seguro + // es delay un frame con cx.spawn + immediate await. + input.update(cx, |i, _| i.request_focus(window)); + + self.rename = Some(RenameState { + original_name, + input, + }); + self.close_menu(cx); + } + + fn close_rename(&mut self, cx: &mut Context) { + if self.rename.take().is_some() { + cx.notify(); + } + } + + fn commit_rename( + &mut self, + target_path: &str, + original_name: &str, + new_name: String, + cx: &mut Context, + ) { + let trimmed = new_name.trim(); + let parent_dir = std::path::Path::new(target_path) + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::path::PathBuf::from(self.root.clone())); + + if !trimmed.is_empty() && trimmed != original_name { + let from = std::path::PathBuf::from(target_path); + let to = parent_dir.join(trimmed); + if let Err(e) = std::fs::rename(&from, &to) { + eprintln!("[FileExplorer] rename {:?} → {:?}: {}", from, to, e); + } + } + + let parent_str = parent_dir.to_string_lossy().into_owned(); + self.refresh_dir(parent_str, cx); + self.close_rename(cx); + } + + fn action_delete( + &mut self, + target: MenuTarget, + window: &mut Window, + cx: &mut Context, + ) { + // Prompt nativo del platform — devuelve un Receiver con el índice + // del botón clickeado. Esperamos el resultado en un task spawn. + let path = target.id.clone(); + let is_folder = target.is_folder; + let name = std::path::Path::new(&path) + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.clone()); + let parent_dir = std::path::Path::new(&path) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_else(|| self.root.clone()); + + let answer = window.prompt( + PromptLevel::Warning, + &format!("¿Borrar \"{}\"?", name), + None, + &["Borrar", "Cancelar"], + cx, + ); + + cx.spawn(async move |this, cx| { + let Ok(idx) = answer.await else { return }; + // 0 = Borrar, 1 = Cancelar. + if idx != 0 { + return; + } + let res = if is_folder { + std::fs::remove_dir_all(&path) + } else { + std::fs::remove_file(&path) + }; + if let Err(e) = res { + eprintln!("[FileExplorer] delete {}: {}", path, e); + } + let _ = this.update(cx, |this, cx| { + this.refresh_dir(parent_dir, cx); + }); + }) + .detach(); + + self.close_menu(cx); + } +} + +// ---- helpers FS ---- + +fn sort_entries(entries: &mut Vec) { + entries.sort_by(|a, b| { + let a_dir = matches!(a.display_type, DisplayType::Folder); + let b_dir = matches!(b.display_type, DisplayType::Folder); + b_dir + .cmp(&a_dir) + .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) + }); +} + +/// Primer nombre `prefix{n}{suffix}` (n = 1..=999) que no exista en `dir`. +/// Para folder no agregamos sufijo. Devuelve None si los 999 nombres ya +/// estaban en uso (improbable). +fn next_available_name( + dir: &str, + prefix: &str, + suffix: &str, + is_folder: bool, +) -> Option { + for n in 1..=999u32 { + let candidate = format!("{}{}{}", prefix, n, suffix); + let full = std::path::Path::new(dir).join(&candidate); + let exists = if is_folder { + full.is_dir() + } else { + full.exists() + }; + if !exists { + return Some(candidate); + } + } + None +} + +// ===================================================================== +// Render +// ===================================================================== + +const MENU_WIDTH: f32 = 200.0; + +impl Render for FileExplorer { + fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = Theme::global(cx).clone(); + let pending_count = self.pending.len(); + + // Capa 1: header + tree. + let header = div() + .h(px(28.0)) + .px(px(8.0)) + .border_b_1() + .border_color(theme.border) + .flex() + .flex_row() + .items_center() + .gap(px(6.0)) + .child( + div() + .text_size(px(11.0)) + .text_color(theme.fg_muted) + .child("📂"), + ) + .child( + div() + .text_size(px(11.0)) + .text_color(theme.fg_text) + .child(SharedString::from(self.root.clone())), + ) + .child( + div() + .ml_auto() + .text_size(px(10.0)) + .text_color(theme.fg_muted) + .child(SharedString::from(if pending_count > 0 { + format!("⏳ {}", pending_count) + } else { + String::new() + })), + ); + + let body = div().flex_grow().min_h(px(0.0)).child(self.tree_view.clone()); + + // Capa 2 (overlay condicional): menú contextual. + let menu_overlay = self.menu.clone().map(|menu| self.render_menu(&theme, menu, cx)); + // Capa 3 (overlay condicional): modal de rename. + let rename_overlay = self.rename.clone().map(|st| self.render_rename(&theme, st)); + + let mut root = div() + .id("file-explorer-root") + .size_full() + .relative() + .bg(theme.bg_panel.clone()) + .flex() + .flex_col() + .child(header) + .child(body); + + if let Some(overlay) = menu_overlay { + root = root.child(overlay); + } + if let Some(overlay) = rename_overlay { + root = root.child(overlay); + } + + root + } +} + +impl FileExplorer { + fn render_menu( + &self, + theme: &Theme, + menu: MenuState, + cx: &mut Context, + ) -> impl IntoElement { + let on_entry = menu.target.is_some(); + let target = menu.target.clone(); + let is_folder = target.as_ref().map(|t| t.is_folder).unwrap_or(false); + + let mut items = div() + .flex() + .flex_col() + .py(px(4.0)) + .min_w(px(MENU_WIDTH)) + .bg(theme.bg_panel_alt.clone()) + .border_1() + .border_color(theme.border_strong) + .rounded(px(6.0)); + + // Open: solo file targets. + if on_entry && !is_folder { + let t = target.clone().unwrap(); + items = items.child( + menu_item("fe-menu-open", "Abrir", theme).on_click(cx.listener( + move |this, _: &ClickEvent, _, cx| { + this.action_open(t.clone(), cx); + }, + )), + ); + } + + // Copy path. + if let Some(t) = target.clone() { + let t_clone = t.clone(); + items = items.child( + menu_item("fe-menu-copy", "Copiar ruta", theme).on_click(cx.listener( + move |this, _: &ClickEvent, w, cx| { + this.action_copy_path(t_clone.clone(), w, cx); + }, + )), + ); + } + + // Rename — solo cuando hay target. + if let Some(t) = target.clone() { + items = items.child( + menu_item("fe-menu-rename", "Renombrar…", theme).on_click(cx.listener( + move |this, _: &ClickEvent, w, cx| { + this.action_rename(t.clone(), w, cx); + }, + )), + ); + items = items.child(separator(theme)); + } + + // New file. + let new_target = target.clone(); + items = items.child( + menu_item("fe-menu-newfile", "Nuevo archivo", theme).on_click(cx.listener( + move |this, _: &ClickEvent, _, cx| { + this.action_new_file(new_target.clone(), cx); + }, + )), + ); + + // New folder. + let new_target_folder = target.clone(); + items = items.child( + menu_item("fe-menu-newfolder", "Nueva carpeta", theme).on_click(cx.listener( + move |this, _: &ClickEvent, _, cx| { + this.action_new_folder(new_target_folder.clone(), cx); + }, + )), + ); + + // Delete: solo entries existentes. + if let Some(t) = target.clone() { + items = items.child(separator(theme)); + items = items.child( + menu_item("fe-menu-delete", "Borrar", theme).on_click(cx.listener( + move |this, _: &ClickEvent, w, cx| { + this.action_delete(t.clone(), w, cx); + }, + )), + ); + } + + // Wrapper absolute en la posición del click. Las coords del + // ContextMenuRequested son window-coords absolutas; las pasamos + // directo con .left/.top. + div() + .absolute() + .left(menu.position.x) + .top(menu.position.y) + .child(items) + } +} + +impl FileExplorer { + fn render_rename(&self, theme: &Theme, state: RenameState) -> impl IntoElement { + // Backdrop oscuro semi-transparente que cubre todo el explorer + + // caja centrada con el TextInput. Click sobre el backdrop NO + // cierra (queremos forzar Enter/Escape para evitar pérdida + // accidental de input). + div() + .absolute() + .top(px(0.0)) + .left(px(0.0)) + .size_full() + .flex() + .items_center() + .justify_center() + .bg(gpui::hsla(0.0, 0.0, 0.0, 0.55)) + .child( + div() + .min_w(px(360.0)) + .p(px(16.0)) + .flex() + .flex_col() + .gap(px(10.0)) + .bg(theme.bg_panel_alt.clone()) + .border_1() + .border_color(theme.border_strong) + .rounded(px(8.0)) + .child( + div() + .text_size(px(13.0)) + .text_color(theme.fg_text) + .child(SharedString::from(format!( + "Renombrar \"{}\"", + state.original_name + ))), + ) + .child(state.input.clone()) + .child( + div() + .text_size(px(10.0)) + .text_color(theme.fg_muted) + .child("Enter = confirmar — Escape = cancelar"), + ), + ) + } +} + +fn menu_item( + id: &'static str, + label: &'static str, + theme: &Theme, +) -> gpui::Stateful { + div() + .id(id) + .px(px(12.0)) + .py(px(6.0)) + .text_size(px(12.0)) + .text_color(theme.fg_text) + .hover(|s| s.bg(theme.bg_row_hover)) + .child(label) +} + +fn separator(theme: &Theme) -> gpui::Div { + div() + .my(px(3.0)) + .h(px(1.0)) + .w_full() + .bg(theme.border) +} diff --git a/crates/apps/image_viewer/Cargo.toml b/crates/apps/image_viewer/Cargo.toml new file mode 100644 index 0000000..4d36de2 --- /dev/null +++ b/crates/apps/image_viewer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "yahweh-image-viewer" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +description = "Visor de imágenes. Suscribe al AppBus y renderea con gpui::img(path)." + +[dependencies] +gpui = { workspace = true } +yahweh-bus = { workspace = true } +yahweh-theme = { workspace = true } diff --git a/crates/apps/image_viewer/src/lib.rs b/crates/apps/image_viewer/src/lib.rs new file mode 100644 index 0000000..c4eb516 --- /dev/null +++ b/crates/apps/image_viewer/src/lib.rs @@ -0,0 +1,148 @@ +//! `yahweh_image_viewer` — visor de imágenes. +//! +//! Suscribe al `AppBus` y, en cada `EntitySelected` cuyo provider sea +//! `local_fs` y la extensión sugiera imagen (jpg, png, webp, gif), pasa el +//! path a `gpui::img(...)` que se encarga del decode + cache. Para otros +//! providers o extensiones desconocidas, muestra un mensaje neutro sin +//! intentar render (evita binarios random pasando por el decoder). +//! +//! Detección por extensión: lista de extensiones soportadas en +//! [`is_image_path`]. Para discriminar por mime real (sin importar la +//! extensión) habría que invocar `image::guess_format` en un task — +//! valdrá la pena cuando carguemos imágenes desde SQLite blobs. + +use std::path::Path; + +use gpui::{ + Context, Entity, IntoElement, Render, SharedString, Window, div, img, prelude::*, px, +}; + +use yahweh_bus::{AppBus, AppEvent}; +use yahweh_theme::Theme; + +const FS_PROVIDER: &str = "local_fs"; + +pub struct ImageViewer { + /// Path actualmente mostrado (si lo hay). + current_path: Option, + /// Mensaje a mostrar cuando no se puede renderear (extensión no + /// reconocida, provider sin soporte, etc.). + notice: Option, +} + +impl ImageViewer { + pub fn new(bus: Entity, cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()).detach(); + cx.subscribe(&bus, |this: &mut ImageViewer, _, ev, cx| { + this.on_app_event(ev, cx); + }) + .detach(); + Self { + current_path: None, + notice: None, + } + } + + fn on_app_event(&mut self, event: &AppEvent, cx: &mut Context) { + let (provider, id) = match event { + AppEvent::EntitySelected { provider, id, .. } + | AppEvent::EntityOpened { provider, id, .. } => (provider, id), + }; + + if provider != FS_PROVIDER { + self.current_path = None; + self.notice = Some( + format!("provider '{}' no soportado por ImageViewer", provider).into(), + ); + cx.notify(); + return; + } + + if !is_image_path(id) { + self.current_path = None; + self.notice = Some("(no es una imagen reconocible)".into()); + cx.notify(); + return; + } + + self.current_path = Some(id.clone()); + self.notice = None; + cx.notify(); + } +} + +fn is_image_path(path: &str) -> bool { + let ext = Path::new(path) + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + matches!( + ext.as_str(), + "png" | "jpg" | "jpeg" | "webp" | "gif" | "bmp" | "ico" | "tiff" | "tif" + ) +} + +impl Render for ImageViewer { + fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = Theme::global(cx).clone(); + + let header_text = match (&self.current_path, &self.notice) { + (Some(p), _) => format!("[image] {}", p), + (None, Some(n)) => n.to_string(), + (None, None) => "(ninguna imagen seleccionada)".to_string(), + }; + + let body: gpui::AnyElement = match (&self.current_path, &self.notice) { + (Some(path), _) => { + let path_buf = std::path::PathBuf::from(path); + div() + .flex_grow() + .min_h(px(0.0)) + .flex() + .items_center() + .justify_center() + .p(px(12.0)) + .child(img(path_buf).max_w_full().max_h_full()) + .into_any_element() + } + (None, Some(n)) => div() + .flex_grow() + .flex() + .items_center() + .justify_center() + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child(n.clone()) + .into_any_element(), + (None, None) => div() + .flex_grow() + .flex() + .items_center() + .justify_center() + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child("doble click sobre una imagen en el FileExplorer.") + .into_any_element(), + }; + + div() + .size_full() + .bg(theme.bg_panel.clone()) + .flex() + .flex_col() + .child( + div() + .h(px(28.0)) + .px(px(10.0)) + .border_b_1() + .border_color(theme.border) + .flex() + .items_center() + .text_size(px(11.0)) + .text_color(theme.fg_muted) + .child(SharedString::from(header_text)), + ) + .child(body) + } +} diff --git a/crates/apps/text_viewer/Cargo.toml b/crates/apps/text_viewer/Cargo.toml new file mode 100644 index 0000000..fd462a7 --- /dev/null +++ b/crates/apps/text_viewer/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "yahweh-text-viewer" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +description = "Visor de texto plano. Suscribe al AppBus y carga contenido async." + +[dependencies] +gpui = { workspace = true } +yahweh-core = { workspace = true } +yahweh-theme = { workspace = true } +yahweh-bus = { workspace = true } +yahweh-provider-fs = { workspace = true } +yahweh-provider-sqlite = { workspace = true } diff --git a/crates/apps/text_viewer/src/lib.rs b/crates/apps/text_viewer/src/lib.rs new file mode 100644 index 0000000..135b40f --- /dev/null +++ b/crates/apps/text_viewer/src/lib.rs @@ -0,0 +1,285 @@ +//! `yahweh_text_viewer` — visor de texto plano. +//! +//! Suscribe al `AppBus` y, en cada `EntitySelected` / `EntityOpened`, +//! decide si el `provider` corresponde a uno que sabe leer (por ahora +//! `local_fs` y `sqlite_db`); si sí, dispara `cx.spawn` con el provider +//! correspondiente para traer el contenido. Mientras carga muestra +//! "(cargando…)"; al terminar lo pinta como texto con saltos de línea +//! preservados. +//! +//! Si el contenido no es válido UTF-8 (binario), muestra los primeros +//! N bytes en hex — útil para preview no ciego sin pretender ser un +//! editor de binarios. + +use std::sync::Arc; + +use gpui::{ + Context, Entity, IntoElement, Render, SharedString, Window, div, prelude::*, px, +}; + +use yahweh_bus::{AppBus, AppEvent}; +use yahweh_core::DataProvider; +use yahweh_provider_fs::{FileDataProvider, PROVIDER_ID as FS_PROVIDER_ID}; +use yahweh_provider_sqlite::{PROVIDER_ID as SQL_PROVIDER_ID, SqliteDataProvider}; +use yahweh_theme::Theme; + +const PREVIEW_HEX_BYTES: usize = 256; +const MAX_TEXT_BYTES: usize = 256 * 1024; + +pub struct TextViewer { + /// Última entidad mostrada. `None` ⇒ pantalla en estado "vacío". + current: Option, + /// Contenido renderizado. Si está cargando se muestra el estado en + /// `current`. + content: Content, + /// Generación monotónica — al cambiar `current` la incrementamos para + /// descartar resultados de loads previos que vuelvan tarde. + generation: u64, +} + +#[derive(Clone, Debug)] +struct CurrentEntity { + provider: String, + provider_path: Option, + id: String, + loading: bool, +} + +#[derive(Clone)] +enum Content { + Empty, + Loading, + Text(SharedString), + HexPreview(SharedString), + Error(SharedString), + Unsupported(SharedString), +} + +impl TextViewer { + pub fn new(bus: Entity, cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()).detach(); + + cx.subscribe(&bus, |this: &mut TextViewer, _, ev, cx| { + this.on_app_event(ev, cx); + }) + .detach(); + + Self { + current: None, + content: Content::Empty, + generation: 0, + } + } + + fn on_app_event(&mut self, event: &AppEvent, cx: &mut Context) { + let (provider, provider_path, id) = match event { + AppEvent::EntitySelected { provider, provider_path, id } + | AppEvent::EntityOpened { provider, provider_path, id } => { + (provider.clone(), provider_path.clone(), id.clone()) + } + }; + + // Comparar con el actual para evitar reload de lo mismo. + if let Some(cur) = &self.current { + if cur.provider == provider && cur.id == id && cur.provider_path == provider_path { + return; + } + } + + self.generation = self.generation.wrapping_add(1); + let gen = self.generation; + self.current = Some(CurrentEntity { + provider: provider.clone(), + provider_path: provider_path.clone(), + id: id.clone(), + loading: true, + }); + self.content = Content::Loading; + cx.notify(); + + // Dispatch por provider. + if provider == FS_PROVIDER_ID { + self.spawn_load_fs(id, gen, cx); + } else if provider == SQL_PROVIDER_ID { + self.spawn_load_sqlite(provider_path, id, gen, cx); + } else { + self.content = Content::Unsupported( + format!("provider '{}' no soportado por TextViewer", provider).into(), + ); + if let Some(cur) = &mut self.current { + cur.loading = false; + } + cx.notify(); + } + } + + fn spawn_load_fs(&self, path: String, gen: u64, cx: &mut Context) { + let provider = Arc::new(FileDataProvider); + cx.spawn(async move |this, cx| { + let result = provider.get_data(&path).await; + let _ = this.update(cx, |this, cx| this.on_loaded(gen, result, cx)); + }) + .detach(); + } + + fn spawn_load_sqlite( + &self, + provider_path: Option, + id: String, + gen: u64, + cx: &mut Context, + ) { + let db_path = provider_path.unwrap_or_else(|| "yahweh.db".to_string()); + cx.spawn(async move |this, cx| { + // El SqliteDataProvider abre la DB en su constructor — si + // falla, reportamos error y salimos. + let provider = match SqliteDataProvider::new(&db_path) { + Ok(p) => p, + Err(e) => { + let _ = this.update(cx, |this, cx| { + this.on_loaded( + gen, + Err(format!("abriendo {}: {}", db_path, e)), + cx, + ) + }); + return; + } + }; + let result = provider.get_data(&id).await; + let _ = this.update(cx, |this, cx| this.on_loaded(gen, result, cx)); + }) + .detach(); + } + + fn on_loaded( + &mut self, + gen: u64, + result: Result, String>, + cx: &mut Context, + ) { + // Si el usuario cambió de selección antes de que volviera el load, + // descartamos este resultado. + if gen != self.generation { + return; + } + + if let Some(cur) = &mut self.current { + cur.loading = false; + } + + self.content = match result { + Ok(bytes) => bytes_to_content(&bytes), + Err(e) => Content::Error(format!("error: {}", e).into()), + }; + cx.notify(); + } +} + +fn bytes_to_content(bytes: &[u8]) -> Content { + if bytes.is_empty() { + return Content::Text("(vacío)".into()); + } + let truncated = bytes.len() > MAX_TEXT_BYTES; + let slice = if truncated { &bytes[..MAX_TEXT_BYTES] } else { bytes }; + match std::str::from_utf8(slice) { + Ok(s) => { + let mut out = s.to_string(); + if truncated { + out.push_str("\n…(truncado)…"); + } + Content::Text(out.into()) + } + Err(_) => { + // No es UTF-8: mostramos hex preview de los primeros bytes. + let n = bytes.len().min(PREVIEW_HEX_BYTES); + let mut hex = String::with_capacity(n * 3); + for (i, b) in bytes[..n].iter().enumerate() { + if i > 0 && i % 16 == 0 { + hex.push('\n'); + } + hex.push_str(&format!("{:02x} ", b)); + } + if bytes.len() > n { + hex.push_str(&format!("\n…({} bytes más)", bytes.len() - n)); + } + Content::HexPreview(hex.into()) + } + } +} + +impl Render for TextViewer { + fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = Theme::global(cx).clone(); + + let header_text = match &self.current { + None => "(ningún archivo seleccionado)".to_string(), + Some(cur) => { + let suffix = if cur.loading { " ⏳" } else { "" }; + format!("[{}] {}{}", cur.provider, cur.id, suffix) + } + }; + + let body: gpui::AnyElement = match &self.content { + Content::Empty => div() + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child("seleccioná un archivo en el FileExplorer o una entry en el DatabaseExplorer.") + .into_any_element(), + Content::Loading => div() + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child("(cargando…)") + .into_any_element(), + Content::Text(s) => div() + .text_color(theme.fg_text) + .text_size(px(12.0)) + .font_family("monospace") + .child(s.clone()) + .into_any_element(), + Content::HexPreview(s) => div() + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .font_family("monospace") + .child(s.clone()) + .into_any_element(), + Content::Error(s) => div() + .text_color(theme.accent_strong) + .text_size(px(11.0)) + .child(s.clone()) + .into_any_element(), + Content::Unsupported(s) => div() + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child(s.clone()) + .into_any_element(), + }; + + div() + .size_full() + .bg(theme.bg_panel.clone()) + .flex() + .flex_col() + .child( + div() + .h(px(28.0)) + .px(px(10.0)) + .border_b_1() + .border_color(theme.border) + .flex() + .items_center() + .text_size(px(11.0)) + .text_color(theme.fg_muted) + .child(SharedString::from(header_text)), + ) + .child( + div() + .id("text-viewer-body") + .flex_grow() + .min_h(px(0.0)) + .overflow_scroll() + .p(px(12.0)) + .child(body), + ) + } +} diff --git a/crates/apps/yahweh-shell/Cargo.toml b/crates/apps/yahweh-shell/Cargo.toml new file mode 100644 index 0000000..3d66cae --- /dev/null +++ b/crates/apps/yahweh-shell/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "yahweh-shell" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +description = "Bootstrap GPUI + LayoutHost de Yahweh." + +[dependencies] +yahweh-core = { workspace = true } +yahweh-theme = { workspace = true } +yahweh-provider-fs = { workspace = true } +yahweh-provider-sqlite = { workspace = true } +yahweh-widget-tree = { workspace = true } +yahweh-widget-container-core = { workspace = true } +yahweh-widget-splitter = { workspace = true } +yahweh-widget-tabs = { workspace = true } +yahweh-widget-tiled = { workspace = true } +yahweh-bus = { workspace = true } +yahweh-file-explorer = { workspace = true } +yahweh-database-explorer = { workspace = true } +yahweh-text-viewer = { workspace = true } +yahweh-image-viewer = { workspace = true } +gpui = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +notify = { workspace = true } + +[[bin]] +name = "yahweh" +path = "src/main.rs" diff --git a/crates/apps/yahweh-shell/src/hot_reload.rs b/crates/apps/yahweh-shell/src/hot_reload.rs new file mode 100644 index 0000000..960b086 --- /dev/null +++ b/crates/apps/yahweh-shell/src/hot_reload.rs @@ -0,0 +1,108 @@ +//! Hot-reload del `layout.json` vía `notify` watcher. +//! +//! Anatomía: +//! 1. Un thread del SO corre el watcher (`notify::recommended_watcher`) que +//! spawnea su propio thread de polling. Cuando detecta cambios en el +//! archivo objetivo, manda `()` por un `std::sync::mpsc::channel`. +//! 2. Una task de gpui (`cx.spawn`) hace `try_recv` cada N ms (timer en el +//! `background_executor`). Si llega algo, relee el JSON y actualiza el +//! `LayoutModel` con `replace_tree`. +//! +//! Esquema separado intencional: notify trabaja en hilos del SO (no +//! integra con el executor de gpui), así que rebotamos vía mpsc para no +//! tocar entities desde threads ajenos. El tradeoff es una latencia de +//! poll N (250ms por default) — imperceptible para edición manual de un +//! JSON. +//! +//! Ignoramos cambios cuando el JSON quedó inválido (parse error) — el +//! `LayerConfig::load_or_default` cae al árbol default. Si querés que la +//! UI muestre el error, agregar un AppEvent::ConfigError y un toast en +//! Fase 8. + +use std::path::PathBuf; +use std::sync::mpsc::{Receiver, channel}; +use std::time::Duration; + +use gpui::{App, AsyncApp, Entity}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; + +use yahweh_core::LayerConfig; + +use crate::layout_model::LayoutModel; + +/// Frecuencia de polling del receiver. 250ms es el sweet spot: +/// suficientemente rápido para sentirse "instantáneo" pero sin gastar CPU. +const POLL_INTERVAL: Duration = Duration::from_millis(250); + +/// Spawnea el watcher + el polling task. Devuelve el `RecommendedWatcher` +/// — el caller debe mantenerlo vivo (drop ⇒ stop watching). Por +/// conveniencia retorna también nada más; el caller suele guardar el +/// watcher en una global o filed-leakeada. +pub fn spawn_watch( + path: PathBuf, + model: Entity, + cx: &mut App, +) -> notify::Result { + let (tx, rx) = channel::<()>(); + + // Watcher: el cierre se ejecuta en el thread que `notify` provee. Solo + // empujamos `()` al canal — el side mpsc maneja toda la lógica. + let mut watcher = notify::recommended_watcher(move |res: notify::Result| { + if let Ok(ev) = res { + // Solo nos interesan modify/create — los Access se ignoran + // para no triggerear en lecturas (ej. cat). + if matches!( + ev.kind, + notify::EventKind::Modify(_) + | notify::EventKind::Create(_) + | notify::EventKind::Remove(_) + ) { + let _ = tx.send(()); + } + } + })?; + + // Watcheamos el directorio padre, no el archivo en sí. Muchos editores + // hacen "rename + create" al guardar (atomic write), lo que rompe + // watching del file directo. Ver el dir y filtrar por path es robusto. + let parent = path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")); + watcher.watch(&parent, RecursiveMode::NonRecursive)?; + + // Spawnea el polling task en el ForegroundExecutor para poder llamar + // model.update sin cross-thread issues. + let path_for_task = path.clone(); + cx.foreground_executor() + .spawn(poll_loop(rx, path_for_task, model, cx.to_async())) + .detach(); + + Ok(watcher) +} + +async fn poll_loop( + rx: Receiver<()>, + path: PathBuf, + model: Entity, + mut cx: AsyncApp, +) { + let timer = cx.background_executor().clone(); + loop { + timer.timer(POLL_INTERVAL).await; + // Drenamos todos los eventos acumulados en este ciclo — + // múltiples writes seguidos colapsan a UN solo reload. + let mut had_event = false; + while rx.try_recv().is_ok() { + had_event = true; + } + if !had_event { + continue; + } + + // Releemos el JSON desde disco. Si parsea bien, replace_tree; + // si no, el `load_or_default` cae al default (no rompe la UI). + let tree = LayerConfig::load_or_default(path.to_string_lossy().as_ref()); + let _ = model.update(&mut cx, |m, cx| m.replace_tree(tree, cx)); + } +} diff --git a/crates/apps/yahweh-shell/src/layout_host.rs b/crates/apps/yahweh-shell/src/layout_host.rs new file mode 100644 index 0000000..a9fe8f1 --- /dev/null +++ b/crates/apps/yahweh-shell/src/layout_host.rs @@ -0,0 +1,576 @@ +//! `LayoutHost` — orquestador del layout dinámico. +//! +//! Lee un `LayerConfig` (raíz del JSON) y construye un árbol de entidades +//! GPUI dispatch-eando por `kind`: +//! +//! | kind | factory | +//! |-------------|----------------------------------------------| +//! | "Split" | `SplitContainer` con dirección + flex | +//! | "Tree" | `ManagedTree` con dataset stub | +//! | "Status" | `StatusPanel` | +//! | (otro) | placeholder textual con el kind y params | +//! +//! Cada entidad se memoiza por `NodeId` (string opcional del JSON o el path +//! sintético `root/child/0`). Mientras el `id` no cambie entre rebuilds, la +//! misma instancia se reusa — esto es lo que permite swappear el `kind` de +//! un container (Split → Tabs → Tiled) preservando los hijos sin reset. +//! +//! En Fase 3 el `LayoutHost` solo reconstruye al inicio. La observación del +//! `LayoutModel` (hot-reload del JSON, mutaciones desde la UI) entra en +//! Fase 7. Pero el diseño ya soporta `rebuild()` idempotente — se invoca +//! cuando el modelo cambia y la memoización mantiene los hijos vivos. + +use std::collections::HashMap; + +use gpui::{ + AnyView, Context, Entity, IntoElement, Render, SharedString, Window, div, prelude::*, +}; + +use yahweh_bus::{AppBus, AppEvent}; +use yahweh_core::{LayerConfig, LayoutDirection, NodeId}; +use yahweh_database_explorer::{DatabaseExplorer, DatabaseExplorerEvent}; +use yahweh_file_explorer::{FileExplorer, FileExplorerEvent}; +use yahweh_image_viewer::ImageViewer; +use yahweh_text_viewer::TextViewer; +use yahweh_widget_container_core::ChildSlot; +use yahweh_widget_splitter::{SplitContainer, SplitEvent}; +use yahweh_widget_tabs::TabContainer; +use yahweh_widget_tiled::{TiledContainer, TiledEvent}; + +use crate::layout_model::{LayoutModel, LayoutModelEvent}; +use crate::managed_tree::ManagedTree; +use crate::persister::Persister; +use crate::status_panel::StatusPanel; + +// ===================================================================== +// LayoutHost +// ===================================================================== + +pub struct LayoutHost { + /// Modelo observable. Cualquier mutación (set_kind, replace_tree) + /// dispara un rebuild idempotente que preserva memoización. + model: Entity, + + /// Bus app-level. Lo distribuimos a viewers (que se subscriben para + /// reaccionar a EntitySelected/Opened) y forwardeamos los eventos + /// tipados de explorers hacia él. + bus: Entity, + + /// Persister adoptado — el LayoutHost mantiene su Entity vivo. Sin + /// strong handle el entity se dropea y la subscripción al model + /// queda inactiva. + #[allow(dead_code)] + persister: Entity, + + /// Memoización de instancias por NodeId. Cada slot guarda la entidad + /// tipada para poder llamarle métodos específicos (set_children en + /// containers, etc.). Se preserva entre rebuilds — eso es lo que + /// permite swappear kind del padre sin perder los hijos. + nodes: HashMap, + + /// La AnyView raíz, computada por el último `rebuild`. El render solo + /// pinta este handle. + root_view: Option, +} + +/// Una entidad concreta instanciada en el árbol. Distinguimos por variante +/// porque cada tipo tiene una API distinta para reaccionar a actualizaciones +/// (set_children para Split, etc.). Las factories devuelven `AnyView` +/// directamente (`AnyView::from(entity.clone())`); esta enum solo guarda +/// la handle tipada para llamadas futuras. +enum NodeSlot { + Split(Entity), + Tabs(Entity), + Tiled(Entity), + Tree(Entity), + FileExplorer(Entity), + DatabaseExplorer(Entity), + TextViewer(Entity), + ImageViewer(Entity), + Status(Entity), + Placeholder(Entity), +} + +impl LayoutHost { + pub fn new( + model: Entity, + bus: Entity, + persister: Entity, + cx: &mut Context, + ) -> Self { + // Subscripción event-filtered: solo rebuildeamos en cambios + // estructurales (set_kind, replace_tree). FlexChanged proviene de + // drags de divisor — el splitter ya tiene el flex aplicado en su + // Vec, rebuildear lo resetearía y rompería el drag. + cx.subscribe(&model, |this, _, ev: &LayoutModelEvent, cx| match ev { + LayoutModelEvent::StructureChanged => this.rebuild(cx), + LayoutModelEvent::FlexChanged => {} + }) + .detach(); + + let mut me = Self { + model, + bus, + persister, + nodes: HashMap::new(), + root_view: None, + }; + me.rebuild(cx); + me + } + + /// Rebuild idempotente: walk del árbol del model, instancia (o reusa) + /// cada nodo, propaga children a los containers. + pub fn rebuild(&mut self, cx: &mut Context) { + // Snapshot del config para no chocar con el borrow al iterar + + // mutar self.nodes. + let cfg = self.model.read(cx).tree().clone(); + let used_ids = std::cell::RefCell::new(Vec::new()); + let view = self.build_node(&cfg, "root", &used_ids, cx); + + // GC: tirar nodos cuyo id ya no aparece en el árbol nuevo. + let used: std::collections::HashSet = + used_ids.into_inner().into_iter().collect(); + self.nodes.retain(|id, _| used.contains(id)); + + self.root_view = Some(view); + cx.notify(); + } + + /// DFS recursivo. `path` se acumula para los nodos que no traen `id` + /// propio en el JSON (`root/0/1` etc) — la sintetización vive en + /// `NodeId::from_layer`. + fn build_node( + &mut self, + cfg: &LayerConfig, + path: &str, + used_ids: &std::cell::RefCell>, + cx: &mut Context, + ) -> AnyView { + let id = NodeId::from_layer(cfg, path); + used_ids.borrow_mut().push(id.clone()); + + match cfg.kind.as_str() { + "Split" => self.build_split(id, cfg, path, used_ids, cx), + "Tabs" => self.build_tabs(id, cfg, path, used_ids, cx), + "Tiled" => self.build_tiled(id, cfg, path, used_ids, cx), + "Tree" => self.build_tree(id, cfg, cx), + "FileExplorer" => self.build_file_explorer(id, cfg, cx), + "DatabaseExplorer" => self.build_database_explorer(id, cfg, cx), + "TextViewer" => self.build_text_viewer(id, cx), + "ImageViewer" => self.build_image_viewer(id, cx), + "Status" => self.build_status(id, cx), + _ => self.build_placeholder(id, cfg, cx), + } + } + + /// Helper común — construye los `ChildSlot`s de un contenedor haciendo + /// recursión sobre los hijos del JSON. Usado por Split / Tabs / Tiled. + fn build_child_slots( + &mut self, + cfg: &LayerConfig, + path: &str, + used_ids: &std::cell::RefCell>, + cx: &mut Context, + ) -> Vec { + let mut slots = Vec::with_capacity(cfg.children.len()); + for (i, child) in cfg.children.iter().enumerate() { + let child_path = format!("{}/{}", path, i); + let child_view = self.build_node(child, &child_path, used_ids, cx); + slots.push(ChildSlot { + id: NodeId::from_layer(child, &child_path), + flex: child.flex_weight() as f32, + label: child.get_param("label").cloned(), + view: child_view, + }); + } + slots + } + + // ------- factories por kind ------- + + fn build_split( + &mut self, + id: NodeId, + cfg: &LayerConfig, + path: &str, + used_ids: &std::cell::RefCell>, + cx: &mut Context, + ) -> AnyView { + let direction = match cfg.layout_direction() { + LayoutDirection::Vertical => LayoutDirection::Vertical, + LayoutDirection::Horizontal => LayoutDirection::Horizontal, + LayoutDirection::Overlay => LayoutDirection::Vertical, // fallback + }; + + // Get-or-create — si ya existe del rebuild anterior y es Split, lo + // reusamos. Si era de otro tipo, lo descartamos. + let entity = match self.nodes.get(&id) { + Some(NodeSlot::Split(e)) => e.clone(), + _ => { + let e = cx.new(|cx| SplitContainer::new(direction, cx)); + // Suscripción a DragEnd para persistir flex al model. + // Usamos el id del Split como ancla para resolver children + // por id en el LayerConfig. + let model = self.model.clone(); + let split_node_id = id.clone(); + cx.subscribe(&e, move |_, split_entity, ev: &SplitEvent, cx| { + if !matches!(ev, SplitEvent::DragEnd) { + return; + } + // Snapshot de los flex actuales del splitter. + let snapshots: Vec<(NodeId, f32)> = split_entity + .read(cx) + .children() + .iter() + .map(|c| (c.id.clone(), c.flex)) + .collect(); + let _ = split_node_id; // (queda disponible si en + // futuro queremos targetear + // el padre directamente). + model.update(cx, |m, cx| { + for (child_id, flex) in snapshots { + m.set_flex(&child_id, flex, cx); + } + }); + }) + .detach(); + self.nodes.insert(id.clone(), NodeSlot::Split(e.clone())); + e + } + }; + + // Sincronizamos la dirección por si el JSON cambió. + entity.update(cx, |s, cx| s.set_direction(direction, cx)); + + let slots = self.build_child_slots(cfg, path, used_ids, cx); + entity.update(cx, |s, cx| s.set_children(slots, cx)); + + AnyView::from(entity) + } + + fn build_tabs( + &mut self, + id: NodeId, + cfg: &LayerConfig, + path: &str, + used_ids: &std::cell::RefCell>, + cx: &mut Context, + ) -> AnyView { + let entity = match self.nodes.get(&id) { + Some(NodeSlot::Tabs(e)) => e.clone(), + _ => { + let e = cx.new(|cx| TabContainer::new(cx)); + self.nodes.insert(id.clone(), NodeSlot::Tabs(e.clone())); + e + } + }; + + let slots = self.build_child_slots(cfg, path, used_ids, cx); + entity.update(cx, |s, cx| s.set_children(slots, cx)); + + AnyView::from(entity) + } + + fn build_tiled( + &mut self, + id: NodeId, + cfg: &LayerConfig, + path: &str, + used_ids: &std::cell::RefCell>, + cx: &mut Context, + ) -> AnyView { + let entity = match self.nodes.get(&id) { + Some(NodeSlot::Tiled(e)) => e.clone(), + _ => { + let e = cx.new(|cx| TiledContainer::new(cx)); + // Drag-to-swap: el TiledContainer emite Reordered cuando + // un drag termina sobre otro tile. Lo commiteamos al + // model swappeando children del padre — el rebuild + // posterior aplicará el nuevo orden preservando los + // entities por NodeId. + let model = self.model.clone(); + let parent_id = id.clone(); + cx.subscribe(&e, move |_, _, ev: &TiledEvent, cx| match ev { + TiledEvent::Reordered { + from_index, + to_index, + .. + } => { + let from = *from_index; + let to = *to_index; + let parent = parent_id.clone(); + model.update(cx, |m, cx| m.swap_children(&parent, from, to, cx)); + } + }) + .detach(); + self.nodes.insert(id.clone(), NodeSlot::Tiled(e.clone())); + e + } + }; + + let slots = self.build_child_slots(cfg, path, used_ids, cx); + entity.update(cx, |s, cx| s.set_children(slots, cx)); + + AnyView::from(entity) + } + + fn build_tree(&mut self, id: NodeId, cfg: &LayerConfig, cx: &mut Context) -> AnyView { + // Param `dataset` selecciona el stub. Default: "sources". + let dataset = cfg + .get_param("dataset") + .cloned() + .unwrap_or_else(|| "sources".to_string()); + + let entity = match self.nodes.get(&id) { + Some(NodeSlot::Tree(e)) => e.clone(), + _ => { + let list_id = SharedString::from(format!("tree-{}", id)); + let dataset_key = SharedString::from(dataset); + let e = cx.new(|cx| ManagedTree::new(list_id, dataset_key, cx)); + self.nodes.insert(id.clone(), NodeSlot::Tree(e.clone())); + e + } + }; + + AnyView::from(entity) + } + + fn build_file_explorer( + &mut self, + id: NodeId, + cfg: &LayerConfig, + cx: &mut Context, + ) -> AnyView { + // Param `root` define el path inicial. Default: "." (cwd). + let root = cfg + .get_param("root") + .cloned() + .unwrap_or_else(|| ".".to_string()); + + let entity = match self.nodes.get(&id) { + Some(NodeSlot::FileExplorer(e)) => e.clone(), + _ => { + let e = cx.new(|cx| FileExplorer::new(root, cx)); + // Forwarder: cuando el explorer emite eventos tipados, los + // traducimos al formato agnóstico del AppBus. + let bus = self.bus.clone(); + cx.subscribe(&e, move |_, _, ev: &FileExplorerEvent, cx| { + let app_ev = match ev { + FileExplorerEvent::FileSelected { path } => { + Some(AppEvent::EntitySelected { + provider: "local_fs".to_string(), + provider_path: None, + id: path.clone(), + }) + } + FileExplorerEvent::FileOpened { path } => { + Some(AppEvent::EntityOpened { + provider: "local_fs".to_string(), + provider_path: None, + id: path.clone(), + }) + } + FileExplorerEvent::RootChanged { .. } => None, + }; + if let Some(ev) = app_ev { + bus.update(cx, |_, cx| cx.emit(ev)); + } + }) + .detach(); + self.nodes + .insert(id.clone(), NodeSlot::FileExplorer(e.clone())); + e + } + }; + AnyView::from(entity) + } + + fn build_database_explorer( + &mut self, + id: NodeId, + cfg: &LayerConfig, + cx: &mut Context, + ) -> AnyView { + // Param `path` define el .sqlite. Default: "yahweh.db" en cwd. + let path = cfg + .get_param("path") + .cloned() + .unwrap_or_else(|| "yahweh.db".to_string()); + + let entity = match self.nodes.get(&id) { + Some(NodeSlot::DatabaseExplorer(e)) => e.clone(), + _ => { + let e = cx.new(|cx| DatabaseExplorer::new(path.clone(), cx)); + // Forwarder al bus; el `provider_path` lleva el path del + // .sqlite para que el TextViewer pueda construir su propio + // SqliteDataProvider de la misma DB. + let bus = self.bus.clone(); + let db_path = path.clone(); + cx.subscribe(&e, move |_, _, ev: &DatabaseExplorerEvent, cx| { + let app_ev = match ev { + DatabaseExplorerEvent::EntitySelected { id } => { + Some(AppEvent::EntitySelected { + provider: "sqlite_db".to_string(), + provider_path: Some(db_path.clone()), + id: id.clone(), + }) + } + DatabaseExplorerEvent::EntityOpened { id } => { + Some(AppEvent::EntityOpened { + provider: "sqlite_db".to_string(), + provider_path: Some(db_path.clone()), + id: id.clone(), + }) + } + }; + if let Some(ev) = app_ev { + bus.update(cx, |_, cx| cx.emit(ev)); + } + }) + .detach(); + self.nodes + .insert(id.clone(), NodeSlot::DatabaseExplorer(e.clone())); + e + } + }; + AnyView::from(entity) + } + + fn build_text_viewer(&mut self, id: NodeId, cx: &mut Context) -> AnyView { + let entity = match self.nodes.get(&id) { + Some(NodeSlot::TextViewer(e)) => e.clone(), + _ => { + let bus = self.bus.clone(); + let e = cx.new(|cx| TextViewer::new(bus, cx)); + self.nodes + .insert(id.clone(), NodeSlot::TextViewer(e.clone())); + e + } + }; + AnyView::from(entity) + } + + fn build_image_viewer(&mut self, id: NodeId, cx: &mut Context) -> AnyView { + let entity = match self.nodes.get(&id) { + Some(NodeSlot::ImageViewer(e)) => e.clone(), + _ => { + let bus = self.bus.clone(); + let e = cx.new(|cx| ImageViewer::new(bus, cx)); + self.nodes + .insert(id.clone(), NodeSlot::ImageViewer(e.clone())); + e + } + }; + AnyView::from(entity) + } + + fn build_status(&mut self, id: NodeId, cx: &mut Context) -> AnyView { + let entity = match self.nodes.get(&id) { + Some(NodeSlot::Status(e)) => e.clone(), + _ => { + let model = self.model.clone(); + let e = cx.new(|cx| StatusPanel::new(model, cx)); + self.nodes.insert(id.clone(), NodeSlot::Status(e.clone())); + e + } + }; + AnyView::from(entity) + } + + fn build_placeholder( + &mut self, + id: NodeId, + cfg: &LayerConfig, + cx: &mut Context, + ) -> AnyView { + // Si ya hay placeholder con esta id pero el kind cambió, lo + // recreamos para reflejar el nuevo `kind` en su mensaje. + let want_kind = cfg.kind.clone(); + let create_new = match self.nodes.get(&id) { + Some(NodeSlot::Placeholder(e)) => { + let same_kind = e.read(cx).kind == want_kind; + !same_kind + } + _ => true, + }; + + if create_new { + let kind_clone = cfg.kind.clone(); + let e = cx.new(|cx| PlaceholderView::new(kind_clone, cx)); + self.nodes + .insert(id.clone(), NodeSlot::Placeholder(e.clone())); + return AnyView::from(e); + } + + // Reuso. + if let Some(NodeSlot::Placeholder(e)) = self.nodes.get(&id) { + return AnyView::from(e.clone()); + } + // Imposible llegar acá si la lógica de arriba está bien, pero + // mantenemos el fallback para no panicar en debug builds. + let kind_clone = cfg.kind.clone(); + let e = cx.new(|cx| PlaceholderView::new(kind_clone, cx)); + self.nodes.insert(id, NodeSlot::Placeholder(e.clone())); + AnyView::from(e) + } +} + +impl Render for LayoutHost { + fn render(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { + // En Fase 3 el árbol se construyó en `new` y queda fijo. Si + // root_view es None (ej. config vacío + GC barrió todo), pintamos + // un placeholder neutro. + match self.root_view.clone() { + Some(v) => div().size_full().child(v), + None => div() + .size_full() + .child("(layout vacío — revisar layout.json)"), + } + } +} + +// ===================================================================== +// PlaceholderView — kind no reconocido +// ===================================================================== + +/// View neutra que se instancia para cualquier `kind` que el LayoutHost no +/// sepa construir. Renderea el `kind` y los params para que sea evidente +/// qué falta implementar — útil mientras se desarrollan kinds nuevos +/// (FileExplorer, Tabs, Tiled, etc.). +pub struct PlaceholderView { + kind: String, +} + +impl PlaceholderView { + pub fn new(kind: String, cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()) + .detach(); + Self { kind } + } +} + +impl Render for PlaceholderView { + fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = yahweh_theme::Theme::global(cx).clone(); + div() + .size_full() + .bg(theme.bg_panel.clone()) + .p(gpui::px(16.0)) + .flex() + .flex_col() + .gap(gpui::px(6.0)) + .child( + div() + .text_color(theme.accent) + .text_size(gpui::px(14.0)) + .child(SharedString::from(format!("⟨ kind: {} ⟩", self.kind))), + ) + .child( + div() + .text_color(theme.fg_muted) + .text_size(gpui::px(11.0)) + .child("(placeholder — kind no implementado todavía)"), + ) + } +} diff --git a/crates/apps/yahweh-shell/src/layout_model.rs b/crates/apps/yahweh-shell/src/layout_model.rs new file mode 100644 index 0000000..3202b96 --- /dev/null +++ b/crates/apps/yahweh-shell/src/layout_model.rs @@ -0,0 +1,140 @@ +//! `LayoutModel` — fuente de verdad mutable del árbol de layout. +//! +//! Distinguimos dos clases de cambio: +//! +//! - [`LayoutModelEvent::StructureChanged`]: cambios que requieren rebuild +//! del árbol de entidades (kind, children, params relevantes). Estos +//! también invocan `cx.notify()` para que los `cx.observe` (StatusPanel, +//! etc.) refresquen. +//! - [`LayoutModelEvent::FlexChanged`]: actualización de `flex` de un +//! nodo (típicamente proviene de un drag de divisor). NO requiere +//! rebuild — el SplitContainer ya tiene el flex aplicado en su Vec; solo +//! nos importa para persistir. Por eso no llamamos `cx.notify()`: solo +//! emitimos el evento, así los `cx.observe` (que rebuilden) se mantienen +//! silenciosos durante el drag. +//! +//! El `Persister` (ver `shell/persister.rs`) se subscribe vía +//! `cx.subscribe` y reacciona a los dos. + +use gpui::{Context, EventEmitter}; + +use yahweh_core::{LayerConfig, NodeId}; + +#[derive(Clone, Debug)] +pub enum LayoutModelEvent { + /// Estructural — kind / children / params. Triggerea rebuild en + /// `LayoutHost` y persist. + StructureChanged, + /// Solo flex de un nodo. Triggerea persist; NO rebuild. + FlexChanged, +} + +pub struct LayoutModel { + tree: LayerConfig, +} + +impl EventEmitter for LayoutModel {} + +impl LayoutModel { + pub fn new(tree: LayerConfig) -> Self { + Self { tree } + } + + pub fn tree(&self) -> &LayerConfig { + &self.tree + } + + /// Reemplazo completo del árbol — para hot-reload del JSON. + pub fn replace_tree(&mut self, tree: LayerConfig, cx: &mut Context) { + self.tree = tree; + cx.emit(LayoutModelEvent::StructureChanged); + cx.notify(); + } + + /// Cambia el `kind` del nodo cuyo id JSON coincide con `target_id`. + pub fn set_kind( + &mut self, + target_id: &NodeId, + new_kind: &str, + cx: &mut Context, + ) { + let changed = mutate_node(&mut self.tree, target_id, &mut |node| { + if node.kind != new_kind { + node.kind = new_kind.to_string(); + true + } else { + false + } + }); + if changed { + cx.emit(LayoutModelEvent::StructureChanged); + cx.notify(); + } + } + + /// Setea el flex de un nodo. Solo emite `FlexChanged` (no notify) — + /// usado al final de un drag de divisor para persistir sin + /// triggerear rebuild. + pub fn set_flex(&mut self, target_id: &NodeId, flex: f32, cx: &mut Context) { + let new_val = Some(flex as f64); + let changed = mutate_node(&mut self.tree, target_id, &mut |node| { + if node.flex != new_val { + node.flex = new_val; + true + } else { + false + } + }); + if changed { + cx.emit(LayoutModelEvent::FlexChanged); + } + } + + /// Intercambia dos children del nodo `parent_id`. Triggerea + /// `StructureChanged` (rebuild + persist), porque cambia el orden de + /// instanciación. Si los índices son iguales o están out-of-bounds, + /// es no-op. + pub fn swap_children( + &mut self, + parent_id: &NodeId, + idx_a: usize, + idx_b: usize, + cx: &mut Context, + ) { + if idx_a == idx_b { + return; + } + let mut did_swap = false; + mutate_node(&mut self.tree, parent_id, &mut |node| { + if idx_a < node.children.len() && idx_b < node.children.len() { + node.children.swap(idx_a, idx_b); + did_swap = true; + true + } else { + false + } + }); + if did_swap { + cx.emit(LayoutModelEvent::StructureChanged); + cx.notify(); + } + } +} + +fn mutate_node( + node: &mut LayerConfig, + target: &NodeId, + f: &mut impl FnMut(&mut LayerConfig) -> bool, +) -> bool { + if let Some(id) = &node.id { + if id == target.as_str() { + return f(node); + } + } + for child in node.children.iter_mut() { + if mutate_node(child, target, f) { + return true; + } + } + false +} diff --git a/crates/apps/yahweh-shell/src/main.rs b/crates/apps/yahweh-shell/src/main.rs new file mode 100644 index 0000000..9a31784 --- /dev/null +++ b/crates/apps/yahweh-shell/src/main.rs @@ -0,0 +1,63 @@ +//! Yahweh — bootstrap GPUI. +//! +//! Fase 6: además del LayoutModel, la Shell crea un `AppBus` (Entity) y se +//! lo pasa al LayoutHost. El bus circula a viewers (TextViewer, +//! ImageViewer) que se subscriben directo, y el LayoutHost forwardea los +//! eventos tipados de los explorers (FileExplorer, DatabaseExplorer) +//! traducidos a AppEvent. + +mod hot_reload; +mod layout_host; +mod layout_model; +mod managed_tree; +mod persister; +mod status_panel; + +use gpui::{App, Application, Bounds, WindowBounds, WindowOptions, prelude::*, px, size}; + +use yahweh_bus::AppBus; +use yahweh_core::LayerConfig; +use yahweh_theme::Theme; + +use crate::layout_host::LayoutHost; +use crate::layout_model::LayoutModel; +use crate::persister::Persister; + +const LAYOUT_PATH: &str = "layout.json"; + +fn main() { + Application::new().run(|cx: &mut App| { + Theme::install_default(cx); + + let config = LayerConfig::load_or_default(LAYOUT_PATH); + let bounds = Bounds::centered(None, size(px(1300.), px(800.)), cx); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_w, cx| { + let model = cx.new(|_| LayoutModel::new(config.clone())); + let bus = cx.new(|_| AppBus); + let persister = cx.new(|cx| { + Persister::new(LAYOUT_PATH.into(), model.clone(), cx) + }); + // Hot-reload: notify watcher en el dir del JSON. El + // watcher debe mantenerse vivo (drop ⇒ stop), así que lo + // movemos a una static atómica vía Box::leak. + match hot_reload::spawn_watch(LAYOUT_PATH.into(), model.clone(), cx) { + Ok(watcher) => { + Box::leak(Box::new(watcher)); + } + Err(e) => { + eprintln!("[hot_reload] no se pudo iniciar watcher: {}", e); + } + } + cx.new(|cx| LayoutHost::new(model, bus, persister, cx)) + }, + ) + .unwrap(); + cx.activate(true); + }); +} diff --git a/crates/apps/yahweh-shell/src/managed_tree.rs b/crates/apps/yahweh-shell/src/managed_tree.rs new file mode 100644 index 0000000..1d8e03d --- /dev/null +++ b/crates/apps/yahweh-shell/src/managed_tree.rs @@ -0,0 +1,287 @@ +//! `ManagedTree` — wrapper de `TreeView` que aporta su propio modelo de +//! datos + estado de expansión. En Fase 3 sirve como stub data-driven (los +//! datasets se eligen vía param `dataset` del JSON). En Fase 4 este patrón +//! se concretiza en `FileExplorer` (TreeView + FsProvider) y +//! `DatabaseExplorer` (TreeView + SqliteProvider). +//! +//! La entidad es completamente Render-able y se entrega al LayoutHost como +//! un AnyView. Los TreeView events (RowClicked, ChevronToggled, …) se +//! traducen acá en cambios al estado de expansión y luego se re-emiten +//! como `ManagedTreeEvent` para que el host (LayoutHost o un App-bus +//! futuro) los consuma. + +use std::collections::HashSet; + +use gpui::{ + Context, Entity, EventEmitter, IntoElement, Render, SharedString, Window, div, prelude::*, +}; + +use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView}; + +// ===================================================================== +// Datasets stub (Fase 3). En Fase 4 los reemplazan los providers reales. +// ===================================================================== + +#[derive(Clone)] +pub struct DemoNode { + pub id: &'static str, + pub label: &'static str, + pub icon: &'static str, + pub children: Vec, +} + +impl DemoNode { + fn leaf(id: &'static str, label: &'static str, icon: &'static str) -> Self { + Self { id, label, icon, children: vec![] } + } + fn branch(id: &'static str, label: &'static str, children: Vec) -> Self { + Self { id, label, icon: "📁", children } + } +} + +/// Resuelve el dataset por `key` (proveniente del param `dataset` del JSON). +/// Cualquier key desconocida cae al stub vacío para no romper el render. +pub fn dataset_for(key: &str) -> DemoNode { + match key { + "sources" => yahweh_sources_tree(), + "deps" => yahweh_deps_tree(), + _ => DemoNode { + id: "unknown", + label: "(dataset desconocido)", + icon: "❓", + children: vec![], + }, + } +} + +fn yahweh_sources_tree() -> DemoNode { + DemoNode::branch( + "src-root", + "yahweh (src)", + vec![ + DemoNode::branch( + "shell", + "shell", + vec![DemoNode::leaf("shell/main.rs", "main.rs", "📄")], + ), + DemoNode::branch( + "widgets", + "widgets", + vec![ + DemoNode::branch( + "widgets/tree", + "tree", + vec![DemoNode::leaf("widgets/tree/lib.rs", "lib.rs", "📄")], + ), + DemoNode::branch( + "widgets/splitter", + "splitter", + vec![DemoNode::leaf("widgets/splitter/lib.rs", "lib.rs", "📄")], + ), + ], + ), + DemoNode::branch( + "libs", + "libs", + vec![ + DemoNode::branch( + "libs/core", + "core", + vec![DemoNode::leaf("libs/core/lib.rs", "lib.rs", "📄")], + ), + DemoNode::branch( + "libs/theme", + "theme", + vec![DemoNode::leaf("libs/theme/lib.rs", "lib.rs", "📄")], + ), + DemoNode::branch( + "libs/providers", + "providers", + vec![ + DemoNode::leaf("libs/providers/fs.rs", "fs.rs", "📄"), + DemoNode::leaf("libs/providers/sqlite.rs", "sqlite.rs", "📄"), + ], + ), + ], + ), + ], + ) +} + +fn yahweh_deps_tree() -> DemoNode { + fn branch(id: &'static str, label: &'static str, children: Vec) -> DemoNode { + DemoNode { id, label, icon: "📦", children } + } + let leaf = DemoNode::leaf; + branch( + "deps-root", + "deps", + vec![ + branch( + "ui", + "ui", + vec![ + leaf("dep:gpui", "gpui 0.2.2", "🧊"), + leaf("dep:gpui-macros", "gpui-macros 0.2.2", "🧊"), + ], + ), + branch( + "async", + "async", + vec![ + leaf("dep:tokio", "tokio 1.x", "🌀"), + leaf("dep:async-trait", "async-trait 0.1", "🌀"), + ], + ), + branch( + "data", + "data", + vec![ + leaf("dep:serde", "serde 1", "🧬"), + leaf("dep:serde_json", "serde_json 1", "🧬"), + leaf("dep:rusqlite", "rusqlite 0.31", "🗃️"), + ], + ), + leaf("dep:notify", "notify 6.1", "👂"), + leaf("dep:uuid", "uuid 1", "🔗"), + ], + ) +} + +// ===================================================================== +// Eventos re-emitidos +// ===================================================================== + +/// Re-emitidos por ManagedTree después de procesar el evento bruto del +/// TreeView interno. Contienen el `dataset_key` para que el host distinga +/// entre múltiples ManagedTrees. +/// +/// Los campos están marcados `dead_code`-ok porque en Fase 3 nadie se +/// suscribe — el LayoutHost los va a consumir en Fase 4 vía un AppBus que +/// reenvíe estos eventos a los viewers (TextViewer / ImageViewer / etc). +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub enum ManagedTreeEvent { + RowClicked { dataset: SharedString, id: String }, + RowDoubleClicked { dataset: SharedString, id: String }, + ContextMenu { dataset: SharedString, id: Option }, +} + +// ===================================================================== +// Widget +// ===================================================================== + +pub struct ManagedTree { + view: Entity, + data: DemoNode, + expanded: HashSet, + dataset_key: SharedString, +} + +impl EventEmitter for ManagedTree {} + +impl ManagedTree { + pub fn new( + list_id: SharedString, + dataset_key: SharedString, + cx: &mut Context, + ) -> Self { + let view = cx.new(|cx| TreeView::new(list_id, cx)); + cx.subscribe(&view, |this: &mut ManagedTree, _, ev, cx| { + this.on_tree_event(ev, cx); + }) + .detach(); + + let data = dataset_for(&dataset_key); + let mut expanded = HashSet::new(); + expanded.insert(data.id.to_string()); + + let me = Self { + view, + data, + expanded, + dataset_key, + }; + me.push_rows(cx); + me + } + + fn push_rows(&self, cx: &mut Context) { + let mut rows = Vec::new(); + flatten(&self.data, 0, &self.expanded, &mut rows); + self.view.update(cx, |tree, cx| tree.set_rows(rows, cx)); + } + + fn on_tree_event(&mut self, event: &TreeEvent, cx: &mut Context) { + match event { + TreeEvent::ChevronToggled(id) => { + let key = id.as_str().to_string(); + if !self.expanded.remove(&key) { + self.expanded.insert(key); + } + self.push_rows(cx); + } + TreeEvent::RowClicked(id) => { + cx.emit(ManagedTreeEvent::RowClicked { + dataset: self.dataset_key.clone(), + id: id.to_string(), + }); + } + TreeEvent::RowDoubleClicked(id) => { + cx.emit(ManagedTreeEvent::RowDoubleClicked { + dataset: self.dataset_key.clone(), + id: id.to_string(), + }); + } + TreeEvent::ContextMenuRequested { id, .. } => { + cx.emit(ManagedTreeEvent::ContextMenu { + dataset: self.dataset_key.clone(), + id: id.as_ref().map(|i| i.to_string()), + }); + } + TreeEvent::ActiveChanged(_) => {} + } + } + + /// Reservado para Fase 4: el LayoutHost lo va a consultar al + /// re-emitir eventos al bus. + #[allow(dead_code)] + pub fn dataset_key(&self) -> &str { + &self.dataset_key + } +} + +impl Render for ManagedTree { + fn render(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.view.clone()) + } +} + +// -------- helpers -------- + +fn flatten( + node: &DemoNode, + depth: u32, + expanded: &HashSet, + out: &mut Vec, +) { + let kind = if node.children.is_empty() { + RowKind::Leaf + } else { + RowKind::Branch + }; + let is_expanded = expanded.contains(node.id); + out.push(TreeRow { + id: RowId::new(node.id), + label: node.label.to_string(), + depth, + kind, + expanded: is_expanded, + icon: Some(node.icon.to_string()), + }); + if is_expanded { + for child in &node.children { + flatten(child, depth + 1, expanded, out); + } + } +} diff --git a/crates/apps/yahweh-shell/src/persister.rs b/crates/apps/yahweh-shell/src/persister.rs new file mode 100644 index 0000000..0b9caa0 --- /dev/null +++ b/crates/apps/yahweh-shell/src/persister.rs @@ -0,0 +1,52 @@ +//! `Persister` — escribe el `LayoutModel` a disco en cada cambio. +//! +//! Es una entity sin estado visible (no se renderea). Solo existe para +//! mantener viva la subscripción al `LayoutModel`. Cualquier evento +//! (`StructureChanged` o `FlexChanged`) dispara una escritura sincrónica +//! al `path` configurado. +//! +//! Hoy NO hay debounce — cada drag de divisor emite UN solo `FlexChanged` +//! al final (en DragEnd, no por frame), y los swaps de kind son acción +//! manual del usuario. Si en el futuro las escrituras se vuelven +//! frecuentes, el lugar para sumar debounce es acá: spawn un task que +//! coalesce events dentro de N ms. + +use std::path::PathBuf; + +use gpui::{Context, Entity}; + +use crate::layout_model::{LayoutModel, LayoutModelEvent}; + +pub struct Persister { + path: PathBuf, +} + +impl Persister { + pub fn new(path: PathBuf, model: Entity, cx: &mut Context) -> Self { + cx.subscribe(&model, |this: &mut Persister, model, _ev: &LayoutModelEvent, cx| { + this.write(model.read(cx).tree()); + }) + .detach(); + Self { path } + } + + fn write(&self, tree: &yahweh_core::LayerConfig) { + let json = tree.serialize_json(); + // Anti-loop: si el contenido en disco ya coincide, skip. Esto + // matters cuando el watcher está corriendo: persister write → + // notify modify → replace_tree → persister write → ... sin esto + // sería un loop infinito de syscalls. + if let Ok(existing) = std::fs::read_to_string(&self.path) { + if existing == json { + return; + } + } + if let Err(e) = std::fs::write(&self.path, json) { + eprintln!( + "[Persister] error escribiendo {}: {}", + self.path.display(), + e + ); + } + } +} diff --git a/crates/apps/yahweh-shell/src/status_panel.rs b/crates/apps/yahweh-shell/src/status_panel.rs new file mode 100644 index 0000000..73abbbe --- /dev/null +++ b/crates/apps/yahweh-shell/src/status_panel.rs @@ -0,0 +1,343 @@ +//! `StatusPanel` — panel lateral con info de la app, switcher de tema y +//! controles de swap del kind del contenedor objetivo (Fase 5). +//! +//! Recibe el `Entity` para mutarlo: cuando el usuario clickea +//! "Tabs", el StatusPanel hace +//! `model.update(cx, |m, cx| m.set_kind(target_id, "Tabs", cx))`. El +//! LayoutHost está suscripto al model y rebuildea preservando los hijos +//! del contenedor por NodeId — eso es lo que demuestra que swappear el +//! contenedor no resetea el contenido (las pestañas activas, expansiones +//! del FileExplorer, scroll position, todo se mantiene). + +use gpui::{ + ClickEvent, Context, Entity, IntoElement, Render, SharedString, Window, div, prelude::*, px, +}; + +use yahweh_core::{LayerConfig, NodeId}; +use yahweh_theme::Theme; + +use crate::layout_model::LayoutModel; + +/// Id del contenedor que el StatusPanel controla con sus botones de swap. +/// Hardcoded por ahora; en el futuro lo podríamos leer de un param del +/// JSON (e.g. `target_id`) para que cualquier StatusPanel apunte al +/// container que querramos. +const SWAP_TARGET_ID: &str = "explorers"; + +const SWAPPABLE_KINDS: &[&str] = &["Split", "Tabs", "Tiled"]; + +/// Path del JSON que el botón "Reload" relee. Mantenemos consistencia con +/// `main.rs` — si llegamos a parametrizarlo, hacerlo en un solo lugar. +const LAYOUT_PATH: &str = "layout.json"; + +pub struct StatusPanel { + model: Entity, + last_event: SharedString, +} + +impl StatusPanel { + pub fn new(model: Entity, cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()).detach(); + // Subscripción al model para refrescar el "kind activo" del swap. + cx.observe(&model, |_, _, cx| cx.notify()).detach(); + Self { + model, + last_event: "(esperando bus app-level — Fase 6)".into(), + } + } + + /// Reservado para Fase 6: el AppBus va a actualizar este texto. + #[allow(dead_code)] + pub fn set_status(&mut self, text: SharedString, cx: &mut Context) { + self.last_event = text; + cx.notify(); + } + + fn cycle_theme(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { + let current = Theme::global(cx).name.to_string(); + Theme::set(cx, Theme::next_after(¤t)); + cx.notify(); + } + + fn pick_theme( + &mut self, + name: SharedString, + _: &ClickEvent, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(theme) = Theme::by_name(&name) { + Theme::set(cx, theme); + cx.notify(); + } + } + + fn swap_container( + &mut self, + kind: SharedString, + _: &ClickEvent, + _: &mut Window, + cx: &mut Context, + ) { + let target = NodeId::new(SWAP_TARGET_ID); + self.model.update(cx, |m, cx| { + m.set_kind(&target, &kind, cx); + }); + } + + fn reload_from_disk( + &mut self, + _: &ClickEvent, + _: &mut Window, + cx: &mut Context, + ) { + // Releemos el JSON. Si el parsing falla, `load_or_default` cae al + // árbol default — no panicamos. La preservación de hijos + // funciona vía el id JSON: lo que matchee, persiste; lo nuevo se + // instancia. + let tree = LayerConfig::load_or_default(LAYOUT_PATH); + self.model.update(cx, |m, cx| m.replace_tree(tree, cx)); + } + + /// Lee el `kind` actual del contenedor objetivo desde el model. Si el + /// id no existe (alguien cambió el JSON), devuelve `None` y no + /// resaltamos ningún chip. + fn current_target_kind(&self, cx: &Context) -> Option { + find_kind(self.model.read(cx).tree(), SWAP_TARGET_ID) + } +} + +fn find_kind(node: &yahweh_core::LayerConfig, target: &str) -> Option { + if let Some(id) = &node.id { + if id == target { + return Some(node.kind.clone()); + } + } + for child in &node.children { + if let Some(k) = find_kind(child, target) { + return Some(k); + } + } + None +} + +impl Render for StatusPanel { + fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = Theme::global(cx).clone(); + let active_theme = theme.name; + let theme_chips: Vec<_> = Theme::all() + .into_iter() + .map(|t| (t.name, t.name == active_theme)) + .collect(); + + let current_kind = self.current_target_kind(cx); + + // Theme chips. + let mut theme_row = div().flex().flex_row().flex_wrap().gap(px(6.0)); + for (name, is_active) in theme_chips { + let bg = if is_active { + theme.bg_row_active + } else { + theme.bg_row_hover + }; + let border = if is_active { + theme.accent_strong + } else { + theme.border + }; + let chip_id = SharedString::from(format!("theme-chip-{}", name)); + let chip_name = SharedString::from(name.to_string()); + theme_row = theme_row.child( + div() + .id(chip_id) + .px(px(10.0)) + .py(px(5.0)) + .rounded(px(4.0)) + .border_1() + .border_color(border) + .bg(bg) + .text_size(px(11.0)) + .text_color(theme.fg_text) + .hover(|s| s.opacity(0.85)) + .child(SharedString::from(name.to_string())) + .on_click(cx.listener(move |this, click, w, cx| { + this.pick_theme(chip_name.clone(), click, w, cx); + })), + ); + } + + // Swap chips — uno por kind (Split / Tabs / Tiled). El activo + // refleja el `kind` actual del nodo objetivo. + let mut swap_row = div().flex().flex_row().gap(px(6.0)); + for &kind in SWAPPABLE_KINDS { + let is_active = current_kind.as_deref() == Some(kind); + let bg = if is_active { + theme.bg_row_active + } else { + theme.bg_row_hover + }; + let border = if is_active { + theme.accent_strong + } else { + theme.border + }; + let chip_id = SharedString::from(format!("swap-chip-{}", kind)); + let kind_str = SharedString::from(kind.to_string()); + swap_row = swap_row.child( + div() + .id(chip_id) + .px(px(12.0)) + .py(px(6.0)) + .rounded(px(4.0)) + .border_1() + .border_color(border) + .bg(bg) + .text_size(px(12.0)) + .text_color(theme.fg_text) + .hover(|s| s.opacity(0.85)) + .child(SharedString::from(kind.to_string())) + .on_click(cx.listener(move |this, click, w, cx| { + this.swap_container(kind_str.clone(), click, w, cx); + })), + ); + } + + div() + .size_full() + .bg(theme.bg_panel_alt.clone()) + .p(px(20.0)) + .flex() + .flex_col() + .gap(px(14.0)) + .child( + div() + .text_size(px(22.0)) + .text_color(theme.accent_strong) + .child("Yahweh — Fase 5"), + ) + .child( + div() + .text_color(theme.fg_muted) + .text_size(px(12.0)) + .child("Contenedores intercambiables sin perder hijos."), + ) + // ----- persistencia + reload ----- + .child( + div() + .mt(px(14.0)) + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child("layout.json:"), + ) + .child( + div() + .flex() + .flex_row() + .items_center() + .gap(px(8.0)) + .child( + div() + .text_size(px(10.0)) + .text_color(theme.fg_muted) + .child( + "auto-save al swap o al soltar un divisor.", + ), + ) + .child( + div() + .id("reload-from-disk") + .px(px(10.0)) + .py(px(4.0)) + .rounded(px(4.0)) + .border_1() + .border_color(theme.border_strong) + .bg(theme.bg_panel.clone()) + .text_color(theme.fg_text) + .text_size(px(11.0)) + .hover(|s| s.opacity(0.85)) + .on_click(cx.listener(Self::reload_from_disk)) + .child("⤓ reload"), + ), + ) + // ----- swap del contenedor 'explorers' ----- + .child( + div() + .mt(px(14.0)) + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child(SharedString::from(format!( + "contenedor '{}' — kind:", + SWAP_TARGET_ID + ))), + ) + .child(swap_row) + .child( + div() + .mt(px(2.0)) + .text_color(theme.fg_muted) + .text_size(px(10.0)) + .child( + "click ⇒ swappea el kind del contenedor padre. Los hijos \ + (FileExplorer, DatabaseExplorer) se preservan: cualquier \ + folder expandido o entry seleccionado sigue así tras el swap.", + ), + ) + // ----- log de evento (placeholder Fase 6) ----- + .child( + div() + .mt(px(16.0)) + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child("último evento (bus Fase 6):"), + ) + .child( + div() + .px(px(10.0)) + .py(px(8.0)) + .bg(theme.bg_panel.clone()) + .border_1() + .border_color(theme.border) + .rounded(px(4.0)) + .text_size(px(11.0)) + .text_color(theme.fg_text) + .child(self.last_event.clone()), + ) + // ----- theme switcher ----- + .child( + div() + .mt(px(20.0)) + .text_color(theme.fg_muted) + .text_size(px(11.0)) + .child("tema activo:"), + ) + .child( + div() + .flex() + .flex_row() + .items_center() + .gap(px(10.0)) + .child( + div() + .text_color(theme.accent) + .text_size(px(15.0)) + .child(SharedString::from(theme.name.to_string())), + ) + .child( + div() + .id("cycle-theme") + .px(px(10.0)) + .py(px(4.0)) + .rounded(px(4.0)) + .border_1() + .border_color(theme.border_strong) + .bg(theme.bg_panel.clone()) + .text_color(theme.fg_text) + .text_size(px(11.0)) + .hover(|s| s.opacity(0.85)) + .on_click(cx.listener(Self::cycle_theme)) + .child("⇄ siguiente"), + ), + ) + .child(theme_row) + } +} diff --git a/crates/core/ente-binfmt-compat/Cargo.toml b/crates/core/ente-binfmt-compat/Cargo.toml new file mode 100644 index 0000000..f972677 --- /dev/null +++ b/crates/core/ente-binfmt-compat/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ente-binfmt-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-binfmt-compat" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/core/ente-binfmt-compat/src/main.rs b/crates/core/ente-binfmt-compat/src/main.rs new file mode 100644 index 0000000..9789d5c --- /dev/null +++ b/crates/core/ente-binfmt-compat/src/main.rs @@ -0,0 +1,109 @@ +//! ente-binfmt-compat: registra handlers de binfmt_misc al boot. +//! +//! systemd-binfmt lee `/usr/lib/binfmt.d/*.conf` y `/etc/binfmt.d/*.conf` y +//! escribe cada línea al kernel via `/proc/sys/fs/binfmt_misc/register`. +//! Esto habilita ejecución transparente de binarios no-ELF (qemu-user, +//! wine, etc). +//! +//! Formato de cada línea: +//! ::::::: +//! +//! Líneas que empiezan con `#` o vacías se ignoran. + +use std::fs; +use std::io::Write; +use std::path::Path; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; + +const REGISTER_PATH: &str = "/proc/sys/fs/binfmt_misc/register"; +const SEARCH_DIRS: &[&str] = &[ + "/usr/lib/binfmt.d", + "/etc/binfmt.d", + "/run/binfmt.d", +]; + +fn main() { + init_tracing(); + info!("ente-binfmt-compat: registrando handlers binfmt_misc"); + + if !Path::new(REGISTER_PATH).exists() { + warn!(path = REGISTER_PATH, "binfmt_misc no montado — skip"); + std::process::exit(0); + } + + let mut registered = 0; + let mut errors = 0; + let mut skipped = 0; + + for dir in SEARCH_DIRS { + if !Path::new(dir).exists() { continue; } + let mut entries: Vec<_> = match fs::read_dir(dir) { + Ok(rd) => rd.filter_map(|e| e.ok()).collect(), + Err(_) => continue, + }; + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + let path = entry.path(); + if path.extension().map(|e| e != "conf").unwrap_or(true) { continue; } + let content = match fs::read_to_string(&path) { + Ok(c) => c, + Err(e) => { warn!(?e, path = %path.display(), "read"); continue; } + }; + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { continue; } + match register(line) { + Ok(name) => { + info!(file = %path.display(), %name, "binfmt registrado"); + registered += 1; + } + Err(e) => { + if e.is_already_exists() { + skipped += 1; + } else { + warn!(?e, file = %path.display(), "registro falló"); + errors += 1; + } + } + } + } + } + } + info!(registered, skipped, errors, "binfmt aplicado"); + if errors > 0 { std::process::exit(1); } +} + +#[derive(Debug)] +struct RegError(std::io::Error); +impl std::fmt::Display for RegError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } +} +impl RegError { + fn is_already_exists(&self) -> bool { + // EEXIST = 17 en Linux. + self.0.raw_os_error() == Some(17) + } +} + +/// Escribe la línea al register file. Devuelve el `name` extraído del +/// primer campo (entre `:` separators) si tuvo éxito. +fn register(line: &str) -> Result { + // Sintaxis: ::::::: + // Field 0 (después del ':' inicial) es el name. + let name = line.split(':').nth(1) + .map(|s| s.to_string()) + .unwrap_or_else(|| "?".into()); + let mut f = fs::OpenOptions::new() + .write(true) + .open(REGISTER_PATH) + .map_err(RegError)?; + f.write_all(line.as_bytes()).map_err(RegError)?; + Ok(name) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_binfmt_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-brain/Cargo.toml b/crates/core/ente-brain/Cargo.toml new file mode 100644 index 0000000..0a6958c --- /dev/null +++ b/crates/core/ente-brain/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ente-brain" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +ente-card = { path = "../ente-card" } +ente-cas = { path = "../ente-cas" } +serde = { workspace = true } +serde_json = { workspace = true } +ulid = { workspace = true } +tokio = { workspace = true } +bincode = { workspace = true } +base64 = { workspace = true } +postcard = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } + +[[example]] +name = "brainctl" +path = "examples/brainctl.rs" diff --git a/crates/core/ente-brain/examples/brainctl.rs b/crates/core/ente-brain/examples/brainctl.rs new file mode 100644 index 0000000..5b051b1 --- /dev/null +++ b/crates/core/ente-brain/examples/brainctl.rs @@ -0,0 +1,234 @@ +//! brainctl: cliente CLI del introspect API. +//! +//! Uso: +//! cargo run --example brainctl -p ente-brain -- list-rules +//! cargo run --example brainctl -p ente-brain -- entropy +//! cargo run --example brainctl -p ente-brain -- top 10 +//! cargo run --example brainctl -p ente-brain -- crystals +//! cargo run --example brainctl -p ente-brain -- crystal-json 0 +//! +//! Path del socket: $ENTE_BRAIN_SOCK o $XDG_RUNTIME_DIR/ente-brain.sock + +use ente_brain::introspect::{call, IntrospectRequest, IntrospectResponse}; +use std::path::{Path, PathBuf}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::UnixStream; + +fn socket_path() -> PathBuf { + if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") { + return p.into(); + } + let runtime = std::env::var("XDG_RUNTIME_DIR") + .unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into())); + format!("{runtime}/ente-brain.sock").into() +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let args: Vec = std::env::args().collect(); + let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("entropy"); + + // Comando especial: streaming. Mantiene la conn abierta y lee frames + // hasta Ctrl-C o EOF del servidor. + if cmd == "stream-audit" || cmd == "stream" { + return run_stream_audit(socket_path()).await; + } + + let req = match cmd { + "list-rules" | "rules" => IntrospectRequest::ListRules, + "entropy" => IntrospectRequest::EntropySnapshot, + "top" => { + let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(10); + IntrospectRequest::TopCorrelations { n } + } + "crystals" => IntrospectRequest::Crystals, + "crystal-json" => { + let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); + IntrospectRequest::CrystalJson { index: i } + } + "promote" => { + let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); + IntrospectRequest::PromoteCrystal { index: i } + } + "remove" => { + let id_s = args.get(2).ok_or_else(|| anyhow::anyhow!("se requiere "))?; + let id: ulid::Ulid = id_s.parse()?; + IntrospectRequest::RemoveRule { id } + } + "audit" => { + let limit: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(20); + IntrospectRequest::ListAudit { limit } + } + "flush-audit" => IntrospectRequest::FlushAudit, + "audit-verify" | "verify" => IntrospectRequest::VerifyAudit, + "replay" => IntrospectRequest::ReplayAudit, + "gc-cas" => IntrospectRequest::GcCas { extra_roots: Vec::new() }, + "patterns" => IntrospectRequest::PatternCrystals, + "reload" => { + let path = args.get(2).cloned(); + IntrospectRequest::ReloadRules { path } + } + other => { + eprintln!("subcomando desconocido: {other}"); + eprintln!("válidos: list-rules | entropy | top | crystals | crystal-json | promote | remove | audit | flush-audit | reload [path]"); + std::process::exit(2); + } + }; + + let path = socket_path(); + let resp = call(&path, req).await?; + print_response(&resp); + Ok(()) +} + +fn print_response(r: &IntrospectResponse) { + match r { + IntrospectResponse::Rules(rs) => { + println!("{} reglas vivas:", rs.len()); + for r in rs { + println!(" {} prio={} kind={} actions={} wildcard={}", + r.id, r.priority, r.event_kind_tag, r.action_count, r.scope_wildcard); + } + } + IntrospectResponse::Rule(rule) => match rule { + Some(r) => println!("{r:#?}"), + None => println!("regla no encontrada"), + }, + IntrospectResponse::Entropy { value_bits, sample_size, distinct_kinds, window_full } => { + println!("Shannon entropy : {value_bits:.4} bits"); + println!("Sample size : {sample_size}"); + println!("Distinct kinds : {distinct_kinds}"); + println!("Window full : {window_full}"); + } + IntrospectResponse::Correlations(entries) => { + println!("{} pares (top, ordenado por co-ocurrencia):", entries.len()); + for e in entries { + println!(" n={:>4} P(b|a)={:.3} PMI={:>6.3}b {} → {}", + e.joint_count, e.conditional_prob, e.pmi_bits, e.a, e.b); + } + } + IntrospectResponse::Crystals(cs) => { + println!("{} cristales detectados:", cs.len()); + for (i, c) in cs.iter().enumerate() { + println!(" [{i}] {:?} → {:?} P={:.3} PMI={:.3}b n={}", + c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support); + } + } + IntrospectResponse::Json(s) => println!("{s}"), + IntrospectResponse::Promoted { rule_id, rule_json } => { + println!("regla creada: {rule_id}"); + println!("--- JSON para auditoría / persistencia ---"); + println!("{rule_json}"); + } + IntrospectResponse::Removed(was_present) => { + if *was_present { println!("regla eliminada"); } + else { println!("regla no encontrada"); } + } + IntrospectResponse::AuditEntries(entries) => { + println!("{} entries de audit log:", entries.len()); + for e in entries { + let prev = e.prev_sha.map(hex_short).unwrap_or_else(|| "—".into()); + let sha = hex_short(e.sha); + println!(" seq={:>4} t={} prev={} sha={} {:?}", + e.seq, e.timestamp_ms, prev, sha, e.action); + } + } + IntrospectResponse::Flushed { written, head_sha, total_flushed } => { + println!("flushed: {written} entries esta pasada, total acumulado: {total_flushed}"); + if let Some(sha) = head_sha { + println!("head sha: {}", hex_long(*sha)); + } + } + IntrospectResponse::Reloaded { count } => { + println!("reload OK: {count} reglas activas tras reload"); + } + IntrospectResponse::Replayed(rep) => { + if let Some(e) = &rep.error { + println!("✗ replay falló: {e}"); + } else { + println!("✓ replay completo — {} actions aplicadas, {} reglas finales", + rep.applied, rep.final_rule_count); + } + } + IntrospectResponse::AuditVerified(rep) => { + if let Some(seq) = rep.broken_at_seq { + println!("✗ verificación FALLÓ tras seq={seq}"); + if let Some(e) = &rep.error { println!(" motivo: {e}"); } + println!(" entries verificadas: {}", rep.verified); + } else { + println!("✓ chain verificada — {} entries íntegras", rep.verified); + if let Some(g) = rep.genesis_sha { println!(" genesis: {}", hex_long(g)); } + } + } + IntrospectResponse::Patterns(ps) => { + println!("{} cristales pattern detectados:", ps.len()); + for p in ps { + match p { + ente_brain::crystallize::PatternCrystal::Burst { kind, count, frequency_per_sec } => { + println!(" burst: {kind:?} count={count} freq={frequency_per_sec:.2} Hz"); + } + ente_brain::crystallize::PatternCrystal::Silence { kind, last_count, since_secs } => { + println!(" silence: {kind:?} last_count={last_count} ausente={since_secs:.1}s"); + } + } + } + } + IntrospectResponse::GcResult { deleted, freed_bytes } => { + println!("CAS gc: {deleted} blobs eliminados, {freed_bytes} bytes liberados"); + } + IntrospectResponse::AuditStreamFrame(_) => { + // En modo request/response no debería llegar; solo aparece en + // run_stream_audit. Si llega aquí es un bug del servidor. + eprintln!("frame de stream recibido fuera de stream-audit (bug)"); + } + IntrospectResponse::Error(e) => eprintln!("error: {e}"), + } +} + +fn hex_short(sha: [u8; 32]) -> String { + sha[..4].iter().map(|b| format!("{:02x}", b)).collect::() + ".." +} + +fn hex_long(sha: [u8; 32]) -> String { + sha.iter().map(|b| format!("{:02x}", b)).collect() +} + +async fn run_stream_audit(path: PathBuf) -> anyhow::Result<()> { + let mut stream = UnixStream::connect(&path).await?; + let req = IntrospectRequest::StreamAudit; + let buf = bincode::serialize(&req)?; + stream.write_u32(buf.len() as u32).await?; + stream.write_all(&buf).await?; + eprintln!("audit stream conectado a {} — Ctrl-C para salir", path.display()); + + loop { + let mut len_buf = [0u8; 4]; + if stream.read_exact(&mut len_buf).await.is_err() { + eprintln!("\nstream cerrado por el servidor"); + return Ok(()); + } + let len = u32::from_be_bytes(len_buf) as usize; + if len > 4 * 1024 * 1024 { anyhow::bail!("frame oversize"); } + let mut buf = vec![0u8; len]; + stream.read_exact(&mut buf).await?; + let resp: IntrospectResponse = bincode::deserialize(&buf)?; + match resp { + IntrospectResponse::AuditStreamFrame(entry) => { + let prev = entry.prev_sha + .map(|s| s[..4].iter().map(|b| format!("{:02x}", b)).collect::() + "..") + .unwrap_or_else(|| "—".into()); + let sha = entry.sha[..4].iter().map(|b| format!("{:02x}", b)) + .collect::() + ".."; + println!("[stream] seq={} prev={} sha={} {:?}", + entry.seq, prev, sha, entry.action); + } + other => { + eprintln!("frame no esperado en stream: {other:?}"); + return Ok(()); + } + } + } +} + +#[allow(dead_code)] +fn _suppress(_: &Path) {} // mantener Path import si compilador se queja diff --git a/crates/core/ente-brain/schema/rule.k b/crates/core/ente-brain/schema/rule.k new file mode 100644 index 0000000..432cde5 --- /dev/null +++ b/crates/core/ente-brain/schema/rule.k @@ -0,0 +1,167 @@ +# ============================================================================ +# rule.k — REFERENCE ONLY. NOT LOADED. +# +# La gramática autoritativa de Rule vive en Rust: +# crates/ente-brain/src/rules.rs +# El loader (crates/ente-brain/src/loader.rs) sólo acepta JSON / JSONL. +# +# Conservado como notas de diseño humano-legibles del shape Rule: +# Triplet [Sujeto + Evento + Acción(Objeto)]. Cada regla es una sinapsis: +# cuando ocurre `when`, el motor ejecuta `then` para los Entes que cumplen +# `scope`. El motor las indexa por discriminante de EventKind para lookup +# en O(1). Las reglas son inmutables tras carga (Arc). +# +# Si cambias el shape en Rust, sincroniza este archivo a mano (o +# reemplázalo por JSON Schema generado vía `schemars`). +# ============================================================================ + +schema Rule: + """Una sinapsis del fractal. Determinista, sin estado entre disparos.""" + id: str # Ulid 26 chars + priority: int = 5 # 0..255, mayor = se ejecuta primero + when: EventPattern + then: [Action] + scope: Scope = Scope {} # qué Entes son sujetos válidos + + check: + len(id) == 26, "id debe ser Ulid" + priority >= 0 and priority <= 255, "priority fuera de rango" + len(then) > 0, "regla sin acciones" + + +# ---------- Subject: alcance del sujeto ---------- + +schema Scope: + """Match del sujeto. None en todos los campos = match cualquier Ente.""" + subject_id?: str # Ulid exacto + subject_label?: str # label exacto + subject_has_cap?: Capability # Ente que declara esta capacidad + + check: + subject_id is None or len(subject_id) == 26, "subject_id no es Ulid" + + +# ---------- Event: qué dispara la regla ---------- + +# EventPattern es tagged union recursivo. +# +# Atómicos: +# Single — match un evento por kind +# Sequence — N eventos consecutivos dentro de within_ms +# +# Compuestos (recursivos): +# Either — OR sobre sub-patterns +# All — AND sobre sub-patterns (mismo event/history) +schema EventPattern: + type: "Single" | "Sequence" | "Either" | "All" + kind?: EventKind # Single + kinds?: [EventKind] # Sequence + within_ms?: int = 0 + patterns?: [EventPattern] # Either / All (recursivo) + + check: + type != "Single" or kind is not None, "Single requiere kind" + type != "Sequence" or (kinds is not None and len(kinds) > 0), \ + "Sequence requiere kinds no vacío" + type != "Either" or (patterns is not None and len(patterns) > 0), \ + "Either requiere patterns no vacío" + type != "All" or (patterns is not None and len(patterns) > 0), \ + "All requiere patterns no vacío" + within_ms is None or within_ms >= 0, "within_ms negativo" + + +# EventKind con tag interno + payload opcional según tag. +schema EventKind: + tag: "EnteSpawned" | "EnteDied" | "BusAnnounce" | "BusInvoke" | "BusInvokeOf" | "DeviceAdded" | "DeviceRemoved" | "Custom" + cap?: Capability # para BusInvokeOf + custom?: str # para Custom + + check: + tag != "BusInvokeOf" or cap is not None, "BusInvokeOf requiere cap" + tag != "Custom" or custom is not None, "Custom requiere custom string" + + +# ---------- Action: qué hacer ---------- + +schema Action: + """Una acción ejecutable por el motor. Tagged union con kind.""" + kind: "Log" | "Notify" | "Spawn" | "Invoke" | "Inhibit" + # Log + level?: "trace" | "debug" | "info" | "warn" | "error" + message?: str + # Notify + target_id?: str # Ulid + # Spawn + card_blob?: str # base64-encoded EntityCard JSON + # Invoke + target_cap?: Capability + blob_b64?: str + # Inhibit + reason?: str + + check: + kind != "Log" or message is not None, "Log requiere message" + kind != "Notify" or (target_id is not None and message is not None), \ + "Notify requiere target_id + message" + kind != "Spawn" or card_blob is not None, "Spawn requiere card_blob" + kind != "Invoke" or target_cap is not None, "Invoke requiere target_cap" + kind != "Inhibit" or reason is not None, "Inhibit requiere reason" + + +# ---------- Capability: re-export desde card.k para evitar inclusión circular ---------- + +# En uso real: `import ..ente_card.schema.card` y referencia Capability. +# Aquí declaramos una versión alineada para auto-contención del esquema. +schema Capability: + kind: "FilesystemRoot" | "KernelNetlink" | "Endpoint" | "LegacyLogind" | "Device" | "Spawn" | "Journal" + netlink_family?: "Uevent" | "Route" | "Generic" | "Audit" + endpoint_interface?: str + endpoint_version?: int + device_class?: "Block" | "Tty" | "Input" | "Drm" | "Net" | "Hidraw" + + +# ============================================================================ +# Ejemplo de regla cristalizada (auto-generada por el observador) +# ============================================================================ + +example_rule = Rule { + id = "01KQQ100000000000000000000" + priority = 5 + when = EventPattern { + type = "Single" + kind = EventKind {tag = "EnteSpawned"} + } + scope = Scope { + subject_label = "demo-echo" + } + then = [ + Action { + kind = "Log" + level = "info" + message = "demo-echo encarnado, observando para crystallization" + } + ] +} + +# Ejemplo de regla compuesta: cuando un Ente se anuncia y luego es invocado +# en menos de 500ms, log estructurado para auditoría. +example_sequence = Rule { + id = "01KQQ200000000000000000000" + priority = 7 + when = EventPattern { + type = "Sequence" + kinds = [ + EventKind {tag = "BusAnnounce"} + EventKind {tag = "BusInvoke"} + ] + within_ms = 500 + } + scope = Scope {} + then = [ + Action { + kind = "Log" + level = "info" + message = "patrón Announce→Invoke detectado <500ms" + } + ] +} diff --git a/crates/core/ente-brain/src/audit.rs b/crates/core/ente-brain/src/audit.rs new file mode 100644 index 0000000..a95e80f --- /dev/null +++ b/crates/core/ente-brain/src/audit.rs @@ -0,0 +1,550 @@ +//! Audit log: cada acción mutadora del cerebro deja una entry inmutable +//! con su predecesor encadenado por SHA256 (estilo Merkle). Verificable a +//! posteriori sin confianza en quien escribe. +//! +//! Los entries viven en memoria. Para persistencia, `flush_to_cas()` los +//! escribe al content-addressable store y devuelve el SHA del head, que +//! puede guardarse en un archivo de "head pointer" (fuera de scope aquí). + +use crate::crystallize::Crystal; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use ulid::Ulid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditEntry { + /// Sequence number monotónico desde el inicio del log. + pub seq: u64, + /// Wall-clock al insertar. + pub timestamp_ms: u64, + /// SHA256 del entry anterior. None para el primer entry. + pub prev_sha: Option<[u8; 32]>, + /// SHA256 de este entry (auto-calculado al construir). + pub sha: [u8; 32], + /// Acción registrada. + pub action: AuditAction, +} + +/// Sin `#[serde(tag)]`: bincode requiere external tagging (default serde +/// para enums) para no usar `deserialize_any`. JSON sigue legible. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuditAction { + PromoteCrystal { rule_id: Ulid, crystal: Crystal }, + RemoveRule { rule_id: Ulid }, + LoadRulesFile { path: String, count: usize }, +} + +pub struct AuditLog { + entries: VecDeque, + next_seq: u64, + /// Cap del log en memoria. Entries más viejos se descartan tras flush. + cap: usize, + /// Total acumulado de entries flusheadas a CAS. + flushed_count: u64, + /// SHA del último entry persistido a CAS — el "head pointer" del log. + last_flushed_sha: Option<[u8; 32]>, + /// Path opcional donde escribir el head pointer tras cada flush. + head_pointer_path: Option, + /// Subscribers a entries en tiempo real. Cada `append` empuja a todos. + /// Subscribers cuyo receiver se dropeó se purgan en el siguiente push. + subscribers: Vec>, + /// Wall-clock del último flush exitoso a CAS. None si aún no se flush. + last_flush_at_ms: Option, +} + +impl AuditLog { + pub fn new() -> Self { + Self::with_cap(512) + } + + pub fn with_cap(cap: usize) -> Self { + Self { + entries: VecDeque::new(), + next_seq: 0, + cap, + flushed_count: 0, + last_flushed_sha: None, + head_pointer_path: None, + subscribers: Vec::new(), + last_flush_at_ms: None, + } + } + + /// Registra un nuevo subscriber. El receiver recibe cada `AuditEntry` + /// futuro hasta que el receiver se dropee (subscriber se purga al + /// siguiente `append`). + pub fn subscribe(&mut self) -> tokio::sync::mpsc::UnboundedReceiver { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + self.subscribers.push(tx); + rx + } + + pub fn subscriber_count(&self) -> usize { self.subscribers.len() } + + pub fn with_head_pointer(mut self, path: std::path::PathBuf) -> Self { + self.head_pointer_path = Some(path); + self + } + + /// Apendea una acción. Calcula el SHA encadenado contra el último entry. + pub fn append(&mut self, action: AuditAction) -> AuditEntry { + let prev_sha = self.entries.back().map(|e| e.sha); + let timestamp_ms = now_ms(); + let seq = self.next_seq; + self.next_seq += 1; + + // Pre-construct con sha en cero, luego calcular sha sobre el + // serializado canónico, luego sobreescribir el campo. + let mut entry = AuditEntry { + seq, timestamp_ms, prev_sha, sha: [0u8; 32], action, + }; + entry.sha = compute_sha(&entry); + + if self.entries.len() >= self.cap { + self.entries.pop_front(); + } + self.entries.push_back(entry.clone()); + // Empujar a subscribers, purgando los muertos in-place. + self.subscribers.retain(|tx| tx.send(entry.clone()).is_ok()); + entry + } + + pub fn recent(&self, limit: usize) -> impl Iterator { + let n = if limit == 0 { self.entries.len() } else { limit.min(self.entries.len()) }; + self.entries.iter().skip(self.entries.len() - n) + } + + pub fn len(&self) -> usize { self.entries.len() } + pub fn is_empty(&self) -> bool { self.entries.is_empty() } + + pub fn head_sha(&self) -> Option<[u8; 32]> { + self.entries.back().map(|e| e.sha) + } + + /// Persiste el entry pasado al CAS y devuelve su SHA. Pensado para + /// snapshots externos — el log en memoria sigue intacto. + pub fn persist_to_cas(entry: &AuditEntry) -> anyhow::Result<[u8; 32]> { + let bytes = serde_json::to_vec(entry)?; + let sha = ente_cas::store(&bytes)?; + Ok(sha) + } + + /// Persiste TODOS los entries actuales al CAS y actualiza el head pointer. + /// Idempotente: re-flushar dos veces da los mismos SHAs (CAS dedup). + /// Devuelve cuántas entries se flushearon en esta pasada. + /// + /// Forma canónica: serializamos `entry` con `sha = [0; 32]` (formato + /// pre-hash). El CAS computa sha256 sobre esos bytes y devuelve un SHA + /// que por construcción coincide con `entry.sha` calculado al append. + pub fn flush_to_cas(&mut self) -> anyhow::Result { + let mut written = 0; + let mut last_sha = self.last_flushed_sha; + for entry in &self.entries { + if entry.seq < self.flushed_count { continue; } + let bytes = canonical_bytes(entry); + let sha = ente_cas::store(&bytes)?; + debug_assert_eq!(sha, entry.sha, + "CAS sha != entry.sha — fórmula canónica rota"); + last_sha = Some(sha); + written += 1; + } + self.flushed_count += written as u64; + self.last_flushed_sha = last_sha; + if written > 0 { + self.last_flush_at_ms = Some(now_ms()); + } + // Persistir head pointer si está configurado. + if let (Some(path), Some(sha)) = (&self.head_pointer_path, last_sha) { + let pointer = AuditHeadPointer { + last_seq: self.next_seq.saturating_sub(1), + last_sha: sha, + flushed_count: self.flushed_count, + timestamp_ms: now_ms(), + }; + let json = serde_json::to_vec_pretty(&pointer)?; + // Escritura atómica: tmp + rename + let tmp = path.with_extension("tmp"); + if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } + std::fs::write(&tmp, json)?; + std::fs::rename(&tmp, path)?; + } + Ok(written) + } + + pub fn flushed_count(&self) -> u64 { self.flushed_count } + pub fn last_flushed_sha(&self) -> Option<[u8; 32]> { self.last_flushed_sha } + pub fn last_flush_at_ms(&self) -> Option { self.last_flush_at_ms } + + /// Segundos transcurridos desde el último flush. None si nunca se flush. + pub fn last_flush_age_secs(&self) -> Option { + let then = self.last_flush_at_ms?; + let now = now_ms(); + Some((now.saturating_sub(then)) as f64 / 1000.0) + } +} + +/// Pointer al head del audit log — escrito atómicamente en disco tras cada +/// flush. Permite verificar la integridad del log sin escanearlo entero: +/// el cliente lee el head, recupera el blob desde CAS, valida `prev_sha` +/// recursivamente hasta el genesis. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditHeadPointer { + pub last_seq: u64, + pub last_sha: [u8; 32], + pub flushed_count: u64, + pub timestamp_ms: u64, +} + +/// Reporte de un replay: número de actions aplicadas + reglas finales. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReplayReport { + pub applied: u64, + pub final_rule_count: usize, + pub error: Option, +} + +/// Reporte de verificación de la cadena audit. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerificationReport { + /// Cuántas entries se recorrieron y verificaron exitosamente. + pub verified: u64, + /// Si hubo error, el seq donde se detectó. + pub broken_at_seq: Option, + /// Detalles del error si hubo. + pub error: Option, + /// SHA del genesis (primer entry; prev_sha = None). + pub genesis_sha: Option<[u8; 32]>, +} + +/// Recorre la cadena del audit log desde `start_sha` hacia atrás vía `prev_sha` +/// hasta el genesis. Para cada entry valida: +/// 1. CAS contiene un blob bajo ese SHA +/// 2. sha256(blob) == SHA esperado (defensa contra tampering del CAS) +/// 3. El blob deserializa a AuditEntry con sha=[0;32] (forma canónica) +/// +/// Devuelve un VerificationReport con el conteo, posibles errores y +/// el SHA del genesis (útil para clientes que quieren cachearlo). +pub fn verify_chain_from_cas(start_sha: [u8; 32]) -> VerificationReport { + let mut current = Some(start_sha); + let mut verified = 0u64; + let mut last_seen: Option = None; + + while let Some(sha) = current { + let path = ente_cas::cas_root().join(ente_cas::hex(&sha)); + let bytes = match std::fs::read(&path) { + Ok(b) => b, + Err(e) => return VerificationReport { + verified, + broken_at_seq: last_seen.as_ref().map(|e| e.seq), + error: Some(format!("CAS read {}: {e}", path.display())), + genesis_sha: None, + }, + }; + // Verificación 1: el blob hashea a la SHA esperada (CAS contract). + let actual = ente_cas::sha256_of(&bytes); + if actual != sha { + return VerificationReport { + verified, + broken_at_seq: last_seen.as_ref().map(|e| e.seq), + error: Some(format!( + "CAS tamper en {}: expected {} got {}", + path.display(), ente_cas::hex(&sha), ente_cas::hex(&actual) + )), + genesis_sha: None, + }; + } + // Verificación 2: deserialize. El blob canónico tiene sha=[0;32]. + let mut entry: AuditEntry = match serde_json::from_slice(&bytes) { + Ok(e) => e, + Err(e) => return VerificationReport { + verified, + broken_at_seq: last_seen.as_ref().map(|e| e.seq), + error: Some(format!("deserialize: {e}")), + genesis_sha: None, + }, + }; + // Re-poblar el sha en el entry para reportar coherentemente. + entry.sha = sha; + verified += 1; + + let prev = entry.prev_sha; + last_seen = Some(entry); + current = prev; + } + + VerificationReport { + verified, + broken_at_seq: None, + error: None, + genesis_sha: last_seen.as_ref().map(|e| e.sha), + } +} + +/// Devuelve el set de SHAs alcanzables desde `start_sha` siguiendo +/// `prev_sha` hasta el genesis. Usado por el GC del CAS para construir +/// las "raíces vivas" del audit log. +pub fn reachable_from_head(start_sha: [u8; 32]) -> std::collections::HashSet<[u8; 32]> { + let mut set = std::collections::HashSet::new(); + let mut current = Some(start_sha); + while let Some(sha) = current { + if !set.insert(sha) { break; } // ciclo (no debería pasar) — corta + let path = ente_cas::cas_root().join(ente_cas::hex(&sha)); + let bytes = match std::fs::read(&path) { Ok(b) => b, Err(_) => break }; + let entry: AuditEntry = match serde_json::from_slice(&bytes) { + Ok(e) => e, Err(_) => break, + }; + current = entry.prev_sha; + } + set +} + +/// Recorre la cadena entera (head→genesis) y reconstruye la lista de +/// actions en orden cronológico (oldest first). Útil tanto para replay +/// como para auditoría retrospectiva. +pub fn collect_chain_from_cas(start_sha: [u8; 32]) -> anyhow::Result> { + let mut entries = Vec::new(); + let mut current = Some(start_sha); + while let Some(sha) = current { + let path = ente_cas::cas_root().join(ente_cas::hex(&sha)); + let bytes = std::fs::read(&path)?; + let mut entry: AuditEntry = serde_json::from_slice(&bytes)?; + entry.sha = sha; + let prev = entry.prev_sha; + entries.push(entry); + current = prev; + } + // entries está en orden head→genesis. Reverse para chronological. + entries.reverse(); + Ok(entries) +} + +/// Aplica las actions de la cadena en orden cronológico contra un engine +/// fresco. PromoteCrystal → insert. RemoveRule → remove. LoadRulesFile → +/// log informativo (los archivos pueden no existir en el ambiente actual). +pub fn replay_chain( + start_sha: [u8; 32], + engine: &mut crate::engine::RuleEngine, +) -> ReplayReport { + let entries = match collect_chain_from_cas(start_sha) { + Ok(es) => es, + Err(e) => return ReplayReport { + applied: 0, final_rule_count: engine.len(), + error: Some(format!("collect chain: {e}")), + }, + }; + let mut applied = 0u64; + for entry in &entries { + match &entry.action { + AuditAction::PromoteCrystal { rule_id, crystal } => { + let mut rule = crate::crystallize::crystal_to_rule(crystal); + rule.id = *rule_id; // preservar identidad histórica + engine.insert(rule); + } + AuditAction::RemoveRule { rule_id } => { + engine.remove(*rule_id); + } + AuditAction::LoadRulesFile { path: _, count: _ } => { + // Los archivos referenciados por path pueden haber cambiado + // o no existir. Log y skip — el replay sólo reconstruye + // promotes/removes que tienen estado en CAS. + } + } + applied += 1; + } + ReplayReport { + applied, + final_rule_count: engine.len(), + error: None, + } +} + +impl Default for AuditLog { + fn default() -> Self { Self::new() } +} + +fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +/// SHA256 sobre el entry en forma canónica (sha=[0;32]). Hash y CAS storage +/// ven los mismos bytes, así que `ente_cas::store(canonical)` devuelve el +/// mismo SHA que `compute_sha(entry)`. +fn compute_sha(entry: &AuditEntry) -> [u8; 32] { + let bytes = canonical_bytes(entry); + ente_cas::sha256_of(&bytes) +} + +/// Forma canónica: el entry serializado JSON con `sha = [0; 32]`. +/// JSON sin pretty-print es determinístico para nuestros tipos. +fn canonical_bytes(entry: &AuditEntry) -> Vec { + let canonical = AuditEntry { + sha: [0u8; 32], + ..entry.clone() + }; + serde_json::to_vec(&canonical).unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chain_links_consecutive_entries() { + let mut log = AuditLog::new(); + let e1 = log.append(AuditAction::RemoveRule { rule_id: Ulid::new() }); + let e2 = log.append(AuditAction::RemoveRule { rule_id: Ulid::new() }); + assert!(e1.prev_sha.is_none()); + assert_eq!(e2.prev_sha, Some(e1.sha)); + assert_ne!(e1.sha, e2.sha); + } + + #[test] + fn seq_monotonic() { + let mut log = AuditLog::new(); + let e1 = log.append(AuditAction::RemoveRule { rule_id: Ulid::new() }); + let e2 = log.append(AuditAction::RemoveRule { rule_id: Ulid::new() }); + assert_eq!(e2.seq, e1.seq + 1); + } + + #[test] + fn cap_evicts_oldest() { + let mut log = AuditLog::with_cap(3); + for _ in 0..5 { + log.append(AuditAction::RemoveRule { rule_id: Ulid::new() }); + } + assert_eq!(log.len(), 3); + // El primer seq superviviente debe ser 2. + assert_eq!(log.recent(0).next().unwrap().seq, 2); + } + + // ---------- Tests de integración con CAS real (en directorio temporal) ---------- + + use crate::engine::RuleEngine; + use std::sync::Mutex; + + /// Lock para serializar tests que mutan ENTE_CAS_ROOT (test threads + /// comparten env vars). Sin esto, dos tests en paralelo pisan el path. + static CAS_TEST_LOCK: Mutex<()> = Mutex::new(()); + + fn with_temp_cas(f: F) { + let _guard = CAS_TEST_LOCK.lock().unwrap(); + let dir = std::env::temp_dir().join(format!("ente-cas-test-{}", Ulid::new())); + std::env::set_var("ENTE_CAS_ROOT", &dir); + let _cleanup = scopeguard(&dir); + f(); + } + + fn scopeguard(dir: &std::path::Path) -> impl Drop + '_ { + struct G<'a>(&'a std::path::Path); + impl<'a> Drop for G<'a> { + fn drop(&mut self) { + std::env::remove_var("ENTE_CAS_ROOT"); + let _ = std::fs::remove_dir_all(self.0); + } + } + G(dir) + } + + fn dummy_crystal(ant: EventKind, con: EventKind) -> Crystal { + Crystal { + antecedent: ant, + consequent: con, + conditional_prob: 0.9, + pmi: 1.5, + support: 7, + gap_stats: None, + } + } + + use crate::rules::EventKind; + + #[test] + fn flush_round_trip_preserves_chain() { + with_temp_cas(|| { + let mut log = AuditLog::new(); + let id1 = Ulid::new(); + let id2 = Ulid::new(); + log.append(AuditAction::PromoteCrystal { + rule_id: id1, + crystal: dummy_crystal(EventKind::EnteSpawned, EventKind::EnteDied), + }); + log.append(AuditAction::PromoteCrystal { + rule_id: id2, + crystal: dummy_crystal(EventKind::BusAnnounce, EventKind::BusInvoke), + }); + log.append(AuditAction::RemoveRule { rule_id: id1 }); + + assert_eq!(log.flush_to_cas().unwrap(), 3); + let head = log.last_flushed_sha().expect("head set"); + let report = verify_chain_from_cas(head); + assert!(report.error.is_none(), "verification failed: {:?}", report.error); + assert_eq!(report.verified, 3); + }); + } + + #[test] + fn replay_reconstructs_engine_state() { + with_temp_cas(|| { + let mut log = AuditLog::new(); + let id1: Ulid = "01KQR3000000000000000000A1".parse().unwrap(); + let id2: Ulid = "01KQR3000000000000000000A2".parse().unwrap(); + let id3: Ulid = "01KQR3000000000000000000A3".parse().unwrap(); + log.append(AuditAction::PromoteCrystal { + rule_id: id1, + crystal: dummy_crystal(EventKind::EnteSpawned, EventKind::EnteDied), + }); + log.append(AuditAction::PromoteCrystal { + rule_id: id2, + crystal: dummy_crystal(EventKind::BusAnnounce, EventKind::BusInvoke), + }); + log.append(AuditAction::PromoteCrystal { + rule_id: id3, + crystal: dummy_crystal(EventKind::DeviceAdded, EventKind::DeviceRemoved), + }); + log.append(AuditAction::RemoveRule { rule_id: id2 }); + log.flush_to_cas().unwrap(); + let head = log.last_flushed_sha().unwrap(); + + let mut engine = RuleEngine::empty(); + let rep = replay_chain(head, &mut engine); + assert!(rep.error.is_none(), "replay error: {:?}", rep.error); + assert_eq!(rep.applied, 4); + assert_eq!(engine.len(), 2, "id2 should be removed, id1 + id3 remain"); + // Ulids preservados + let ids: Vec = engine.rules().map(|r| r.id).collect(); + assert!(ids.contains(&id1)); + assert!(!ids.contains(&id2)); + assert!(ids.contains(&id3)); + }); + } + + #[test] + fn replay_after_eviction_still_works() { + with_temp_cas(|| { + // Cap pequeño: la mayoría de entries se evictan de memoria pero + // siguen en CAS. Replay debe poder reconstruir desde CAS solo. + let mut log = AuditLog::with_cap(2); + let mut ids = Vec::new(); + for _ in 0..6 { + let id = Ulid::new(); + ids.push(id); + log.append(AuditAction::PromoteCrystal { + rule_id: id, + crystal: dummy_crystal(EventKind::EnteSpawned, EventKind::EnteDied), + }); + log.flush_to_cas().unwrap(); + } + assert_eq!(log.len(), 2, "cap eviction limita memoria"); + let head = log.last_flushed_sha().unwrap(); + + let mut engine = RuleEngine::empty(); + let rep = replay_chain(head, &mut engine); + assert!(rep.error.is_none()); + assert_eq!(rep.applied, 6); + assert_eq!(engine.len(), 6); + }); + } +} diff --git a/crates/core/ente-brain/src/autopromote.rs b/crates/core/ente-brain/src/autopromote.rs new file mode 100644 index 0000000..a54929e --- /dev/null +++ b/crates/core/ente-brain/src/autopromote.rs @@ -0,0 +1,102 @@ +//! Autopromote loop. Background task que cada N segundos detecta cristales +//! con thresholds altos y los promueve sin intervención humana. +//! +//! Anti-doble-promote: tras promover, registramos en un set la pareja +//! (antecedent_kind, consequent_kind). Antes de promover, verificamos que +//! no exista ya una regla con el mismo trigger_kind (heurística simple — +//! evita ráfagas de duplicados de la misma estadística). + +use crate::audit::AuditAction; +use crate::crystallize::{crystal_to_rule, detect_crystals, Crystal, CrystallizationParams}; +use crate::introspect::{append_rule_jsonl, BrainState}; +use crate::rules::EventKind; +use std::collections::HashSet; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; +use tracing::{info, warn}; + +#[derive(Debug, Clone, Copy)] +pub struct AutopromoteParams { + pub interval_secs: u64, + pub threshold: CrystallizationParams, +} + +impl Default for AutopromoteParams { + fn default() -> Self { + Self { + interval_secs: 60, + // Más estrictos que el threshold default — evitar ruido. + threshold: CrystallizationParams { + min_support: 10, + min_conditional_prob: 0.85, + min_pmi: 2.0, + }, + } + } +} + +/// Spawn del bucle. El handle Mutex evita que dos pasadas concurrentes +/// promuevan el mismo cristal (el lock garantiza serialización por brain). +pub fn spawn_autopromote_loop(state: BrainState, params: AutopromoteParams) { + let promoted_keys: Arc>> = + Arc::new(Mutex::new(HashSet::new())); + + tokio::spawn(async move { + let mut tick = tokio::time::interval(Duration::from_secs(params.interval_secs)); + tick.tick().await; // descartar primer tick inmediato + info!(?params, "autopromote loop activo"); + loop { + tick.tick().await; + run_one_pass(&state, ¶ms, &promoted_keys).await; + } + }); +} + +async fn run_one_pass( + state: &BrainState, + params: &AutopromoteParams, + promoted_keys: &Arc>>, +) { + let crystals: Vec = { + let obs = state.observer.read().await; + detect_crystals(&obs, ¶ms.threshold) + }; + if crystals.is_empty() { return; } + + let mut pk = promoted_keys.lock().await; + for c in crystals { + let key = (c.antecedent.clone(), c.consequent.clone()); + if pk.contains(&key) { + // Ya promovido — el observer puede seguir reportando este + // cristal pero no necesitamos otra regla. + continue; + } + promote_one(state, &c).await; + pk.insert(key); + } +} + +async fn promote_one(state: &BrainState, c: &Crystal) { + let rule = crystal_to_rule(c); + let rule_id = rule.id; + if let Some(path) = state.rules_out.as_ref() { + if let Err(e) = append_rule_jsonl(path, &rule) { + warn!(?e, "autopromote: rules_out append falló"); + } + } + state.engine.write().await.insert(rule); + + state.audit.write().await.append(AuditAction::PromoteCrystal { + rule_id, + crystal: c.clone(), + }); + info!( + %rule_id, + antecedent = ?c.antecedent, + consequent = ?c.consequent, + cp = c.conditional_prob, + pmi = c.pmi, + "autopromote: cristal → regla" + ); +} diff --git a/crates/core/ente-brain/src/crystallize.rs b/crates/core/ente-brain/src/crystallize.rs new file mode 100644 index 0000000..6a90660 --- /dev/null +++ b/crates/core/ente-brain/src/crystallize.rs @@ -0,0 +1,244 @@ +//! Cristalización: del flujo observado a reglas explícitas. +//! +//! Detecta pares (a, b) donde: +//! - support(a, b) ≥ min_support (suficientes muestras para no ser ruido) +//! - P(b|a) ≥ min_conditional_prob (a predice b con confianza) +//! - PMI(a; b) ≥ min_pmi (más correlacionados que random) +//! +//! Cada cristal se materializa como `Rule` ejecutable (`crystal_to_rule`). +//! Para persistencia/transporte, `crystal_to_json_pretty` serializa la Rule +//! resultante con serde — sin formatos intermedios. + +use crate::observer::{GapStats, Observer}; +use crate::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope}; +use serde::{Deserialize, Serialize}; +use std::time::Instant; +use ulid::Ulid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Crystal { + pub antecedent: EventKind, + pub consequent: EventKind, + pub conditional_prob: f64, + pub pmi: f64, + pub support: u64, + /// Estadísticas del gap temporal entre antecedent → consequent. + /// None si no hay histograma. Habilita generación de reglas Sequence + /// con `within_ms = (mean + 2σ) * 1000`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gap_stats: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct CrystallizationParams { + pub min_support: u64, + pub min_conditional_prob: f64, + pub min_pmi: f64, +} + +impl Default for CrystallizationParams { + fn default() -> Self { + Self { + min_support: 5, + min_conditional_prob: 0.7, + min_pmi: 0.5, + } + } +} + +pub fn detect_crystals(obs: &Observer, params: &CrystallizationParams) -> Vec { + let mut out = Vec::new(); + for ((a, b), &count) in obs.cooccurrences() { + if count < params.min_support { continue; } + let cp = obs.conditional_prob(a, b); + if cp < params.min_conditional_prob { continue; } + let mi = obs.pmi(a, b); + if mi < params.min_pmi { continue; } + // Stats del histograma si existen para este par. + let gap_stats = obs.gap_histograms() + .get(&(a.clone(), b.clone())) + .map(|h| h.stats()); + out.push(Crystal { + antecedent: a.clone(), + consequent: b.clone(), + conditional_prob: cp, + pmi: mi, + support: count, + gap_stats, + }); + } + out.sort_by(|x, y| y.conditional_prob.partial_cmp(&x.conditional_prob).unwrap_or(std::cmp::Ordering::Equal)); + out +} + +/// Serializa la `Rule` derivada del cristal como JSON pretty-printed. Ese +/// JSON es el formato canónico de persistencia: el loader lo lee como una +/// línea de JSONL o como elemento de un array. Los stats del cristal (P, PMI, +/// support) viven en el audit log vía `AuditAction::PromoteCrystal`, no se +/// duplican aquí. +pub fn crystal_to_json_pretty(c: &Crystal) -> String { + serde_json::to_string_pretty(&crystal_to_rule(c)) + .expect("Rule serialize should never fail") +} + +/// Convierte un cristal a una `Rule` ejecutable. Si hay gap_stats con +/// muestras suficientes (≥ 4), genera una regla `Sequence` con +/// `within_ms = (mean + 2σ) * 1000`. 2σ cubre ~95% de la distribución +/// asumiendo normalidad — captura el "tiempo típico de respuesta" del +/// patrón observado. Si no hay stats, fallback a `Single { antecedent }`. +pub fn crystal_to_rule(c: &Crystal) -> Rule { + let when = match &c.gap_stats { + Some(s) if s.count >= 4 => { + // Mínimo 1ms para evitar within_ms=0 cuando varianza colapsa. + let bound_secs = (s.mean_secs + 2.0 * s.stddev_secs).max(0.001); + EventPattern::Sequence { + kinds: vec![c.antecedent.clone(), c.consequent.clone()], + within_ms: (bound_secs * 1000.0).ceil() as u64, + } + } + _ => EventPattern::Single { kind: c.antecedent.clone() }, + }; + let message = match &c.gap_stats { + Some(s) if s.count >= 4 => format!( + "crystal seq: {:?} → {:?} (P={:.2}, PMI={:.2}, gap={:.3}±{:.3}s)", + c.antecedent, c.consequent, c.conditional_prob, c.pmi, + s.mean_secs, s.stddev_secs, + ), + _ => format!( + "crystal: {:?} → {:?} (P={:.2}, PMI={:.2}, n={})", + c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support + ), + }; + Rule { + id: Ulid::new(), + priority: 5, + when, + scope: Scope::default(), + then: vec![Action::Log { level: LogLevel::Info, message }], + } +} + + +// ============================================================================ +// Patrones extendidos: Burst (alta frecuencia) y Silence (ausencia prolongada). +// Estos cristales son sobre un único kind, no pares — capturan dinámicas +// temporales de eventos individuales. +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PatternCrystal { + /// Mismo evento aparece con frecuencia alta. `frequency_per_sec` se + /// estima sobre el window de observación. + Burst { + kind: EventKind, + count: u64, + frequency_per_sec: f64, + }, + /// Evento que dejó de aparecer. `since_secs` es el tiempo desde la + /// última observación. + Silence { + kind: EventKind, + last_count: u64, + since_secs: f64, + }, +} + +#[derive(Debug, Clone, Copy)] +pub struct PatternParams { + /// Mínimo de ocurrencias para considerar Burst. + pub burst_min_count: u64, + /// Frecuencia mínima (eventos por segundo) para considerar Burst. + pub burst_min_freq_hz: f64, + /// Tiempo desde última ocurrencia para considerar Silence. + pub silence_min_secs: f64, + /// Mínimo total previo para considerar Silence (eventos < N son ruido). + pub silence_min_prior_count: u64, +} + +impl Default for PatternParams { + fn default() -> Self { + Self { + burst_min_count: 10, + burst_min_freq_hz: 5.0, + silence_min_secs: 30.0, + silence_min_prior_count: 3, + } + } +} + +/// Detecta Bursts y Silences sobre la distribución marginal del observer. +/// La frecuencia de un Burst se aproxima asumiendo que la observación cubre +/// el rango entre `last_seen` y `Instant::now()` para ese kind. +pub fn detect_pattern_crystals(obs: &Observer, params: &PatternParams) -> Vec { + let mut out = Vec::new(); + let now = Instant::now(); + for (kind, &count) in obs.marginals() { + let last_seen = obs.last_seen_marginal(kind); + // ---- Burst ---- + if count >= params.burst_min_count { + // Aproximación: si vimos `count` eventos hasta `last_seen`, y el + // primer evento sucedió en algún momento del window, la freq es + // count / window_age. Sin tiempo del primer evento, usamos + // last_seen → now como denominador (subestima freq) o asumimos + // ventana fija de 60s. Usamos la última como aproximación. + let elapsed = last_seen + .map(|t| now.saturating_duration_since(t).as_secs_f64().max(0.001)) + .unwrap_or(60.0); + // Estimación conservadora: count / max(window_age, 1s). + // Si tenemos histograma, podríamos refinar — TODO. + let freq = count as f64 / elapsed.max(1.0); + if freq >= params.burst_min_freq_hz { + out.push(PatternCrystal::Burst { + kind: kind.clone(), + count, + frequency_per_sec: freq, + }); + } + } + // ---- Silence ---- + if count >= params.silence_min_prior_count { + if let Some(t) = last_seen { + let since = now.saturating_duration_since(t).as_secs_f64(); + if since >= params.silence_min_secs { + out.push(PatternCrystal::Silence { + kind: kind.clone(), + last_count: count, + since_secs: since, + }); + } + } + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rules::EventKind::*; + + #[test] + fn detects_perfect_correlation() { + let mut obs = Observer::new(100); + for _ in 0..10 { + obs.record(EnteSpawned); + obs.record(EnteDied); + } + let crystals = detect_crystals(&obs, &CrystallizationParams { + min_support: 3, + min_conditional_prob: 0.5, + min_pmi: 0.0, + }); + assert!(crystals.iter().any(|c| matches!(c.antecedent, EnteSpawned) + && matches!(c.consequent, EnteDied))); + } + + #[test] + fn rejects_below_threshold() { + let mut obs = Observer::new(100); + // Sin co-ocurrencia significativa. + for _ in 0..3 { obs.record(EnteSpawned); } + let crystals = detect_crystals(&obs, &CrystallizationParams::default()); + assert!(crystals.is_empty(), "no debería haber cristales: {:?}", crystals); + } +} diff --git a/crates/core/ente-brain/src/dispatch.rs b/crates/core/ente-brain/src/dispatch.rs new file mode 100644 index 0000000..f864300 --- /dev/null +++ b/crates/core/ente-brain/src/dispatch.rs @@ -0,0 +1,73 @@ +//! Despacho asíncrono de Actions. El motor entrega `Vec>` matched; +//! este módulo las traduce a efectos del fractal vía un `ActionSink` trait. +//! +//! Esto invierte la dependencia: ente-brain no conoce a ente-zero. El init +//! implementa `ActionSink` y wirea spawn/invoke/log a sus propias estructuras. + +use crate::rules::{Action, LogLevel, Rule}; +use std::sync::Arc; +use tracing::{debug, error, info, trace, warn}; + +/// Backend de ejecución de Actions. ente-zero implementa esto delegando a +/// graph_tx (Spawn → SpawnRequest, Invoke → bus call, etc.). +pub trait ActionSink: Send + Sync { + /// Spawn una Card decodificada. Implementación: GraphEvent::SpawnRequest. + fn spawn(&self, card_blob: &str); + /// Invoke por bus. blob crudo; el sink lo enruta vía bus_mediator. + fn invoke(&self, target_cap: ente_card::Capability, blob: Vec); + /// Notifica a un Ente específico (target_id). Implementación: forward por bus. + fn notify(&self, target_id: ulid::Ulid, message: &str); + /// Inhibe un comportamiento (placeholder; semántica depende del sink). + fn inhibit(&self, reason: &str); +} + +/// Sink por defecto que sólo logea. Útil para tests y dev sin runtime. +pub struct NullSink; + +impl ActionSink for NullSink { + fn spawn(&self, card_blob: &str) { + info!(blob_len = card_blob.len(), "NullSink::spawn (no-op)"); + } + fn invoke(&self, target_cap: ente_card::Capability, blob: Vec) { + info!(?target_cap, blob_len = blob.len(), "NullSink::invoke (no-op)"); + } + fn notify(&self, target_id: ulid::Ulid, message: &str) { + info!(%target_id, %message, "NullSink::notify (no-op)"); + } + fn inhibit(&self, reason: &str) { + info!(%reason, "NullSink::inhibit (no-op)"); + } +} + +/// Ejecuta las reglas matched. Cada Rule puede tener N Actions; ejecutamos +/// todas. Las acciones de Log se evalúan inline (tracing es async-safe). +/// Las acciones de Spawn/Invoke/Notify se delegan al sink — el sink decide +/// si procesarlas sincrónica o asincrónicamente. +pub async fn dispatch_actions(rules: &[Arc], sink: &dyn ActionSink) { + for rule in rules { + trace!(id = %rule.id, priority = rule.priority, n = rule.then.len(), "dispatching rule"); + for action in &rule.then { + execute_action(action, sink, rule.id).await; + } + } +} + +async fn execute_action(action: &Action, sink: &dyn ActionSink, rule_id: ulid::Ulid) { + match action { + Action::Log { level, message } => emit_log(level, message, rule_id), + Action::Notify { target_id, message } => sink.notify(*target_id, message), + Action::Spawn { card_blob } => sink.spawn(card_blob), + Action::Invoke { target_cap, blob } => sink.invoke(target_cap.clone(), blob.clone()), + Action::Inhibit { reason } => sink.inhibit(reason), + } +} + +fn emit_log(level: &LogLevel, message: &str, rule_id: ulid::Ulid) { + match level { + LogLevel::Trace => trace!(rule = %rule_id, "{}", message), + LogLevel::Debug => debug!(rule = %rule_id, "{}", message), + LogLevel::Info => info! (rule = %rule_id, "{}", message), + LogLevel::Warn => warn! (rule = %rule_id, "{}", message), + LogLevel::Error => error!(rule = %rule_id, "{}", message), + } +} diff --git a/crates/core/ente-brain/src/engine.rs b/crates/core/ente-brain/src/engine.rs new file mode 100644 index 0000000..e70ca0f --- /dev/null +++ b/crates/core/ente-brain/src/engine.rs @@ -0,0 +1,399 @@ +//! Motor de inferencia. HashMap>> para +//! lookup O(1) por tipo de evento, luego filter lineal por scope + filtros +//! del payload (BusInvokeOf, Custom). +//! +//! Inmutabilidad fractal: `Arc` es el unit de compartición. Clonar una +//! regla del motor para entregarla al dispatcher es un refcount bump, no copia. + +use crate::observer::TimedEvent; +use crate::rules::{EventKind, EventPattern, Rule, Scope}; +use ente_card::Capability; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use ulid::Ulid; + +/// Discriminante barato de `EventKind` para indexar el HashMap. Sin payload — +/// el match de payload se hace en una segunda pasada lineal en O(k) donde k +/// es el número de reglas para ese tag. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EventKindDiscriminant { + EnteSpawned, + EnteDied, + BusAnnounce, + BusInvoke, + BusInvokeOf, + DeviceAdded, + DeviceRemoved, + Custom, +} + +impl From<&EventKind> for EventKindDiscriminant { + fn from(k: &EventKind) -> Self { + match k { + EventKind::EnteSpawned => Self::EnteSpawned, + EventKind::EnteDied => Self::EnteDied, + EventKind::BusAnnounce => Self::BusAnnounce, + EventKind::BusInvoke => Self::BusInvoke, + EventKind::BusInvokeOf(_) => Self::BusInvokeOf, + EventKind::DeviceAdded => Self::DeviceAdded, + EventKind::DeviceRemoved => Self::DeviceRemoved, + EventKind::Custom(_) => Self::Custom, + } + } +} + +/// Snapshot del Ente que disparó el evento. Necesario para evaluar `Scope`. +#[derive(Debug, Clone, Default)] +pub struct SubjectInfo { + pub id: Option, + pub label: Option, + pub capabilities: Vec, +} + +pub struct RuleEngine { + rules: Vec>, + /// Reglas atómicas (Single, Sequence) indexadas por discriminante del + /// kind que las dispara. Lookup O(1). + by_kind: HashMap>>, + /// Reglas compuestas (Either, All): se evalúan contra cada evento. + /// Para fractales con N pequeño no afecta perf; con N grande, optimizar + /// emitiendo a múltiples buckets en insert (fan-out). + compound: Vec>, +} + +impl Default for RuleEngine { + fn default() -> Self { Self::empty() } +} + +impl RuleEngine { + pub fn empty() -> Self { + Self { rules: Vec::new(), by_kind: HashMap::new(), compound: Vec::new() } + } + + /// Carga reglas desde JSON (lista de Rule). + pub fn load_json(json: &str) -> anyhow::Result { + let rules: Vec = serde_json::from_str(json)?; + let mut engine = Self::empty(); + for r in rules { + r.validate().map_err(|e| anyhow::anyhow!("regla inválida: {e}"))?; + engine.insert(r); + } + Ok(engine) + } + + pub fn insert(&mut self, rule: Rule) { + let arc = Arc::new(rule); + // Atómicas → bucket por discriminante. Compuestas → bucket fallback. + if let Some(trigger) = arc.when.trigger_kind() { + let disc = EventKindDiscriminant::from(trigger); + self.by_kind.entry(disc).or_default().push(arc.clone()); + } else { + self.compound.push(arc.clone()); + } + self.rules.push(arc); + } + + pub fn remove(&mut self, id: Ulid) -> bool { + let before = self.rules.len(); + self.rules.retain(|r| r.id != id); + for v in self.by_kind.values_mut() { + v.retain(|r| r.id != id); + } + self.compound.retain(|r| r.id != id); + before != self.rules.len() + } + + pub fn rules(&self) -> impl Iterator> { self.rules.iter() } + + pub fn len(&self) -> usize { self.rules.len() } + pub fn is_empty(&self) -> bool { self.rules.is_empty() } + + /// Despacho determinista. Devuelve reglas que matchean, ordenadas por + /// prioridad descendente. Cada Arc se clona (refcount) — sin copiar + /// los datos. + /// + /// `history` es el slice de eventos recientes (en orden cronológico, + /// más reciente al final) usado para evaluar Sequence patterns. + /// Para reglas Single, history se ignora. + /// + /// Si el evento es `BusInvokeOf(_)`, también consultamos el bucket + /// `BusInvoke` (regla genérica que ignora la cap). + pub fn dispatch( + &self, + event: &EventKind, + subject: &SubjectInfo, + history: &[TimedEvent], + ) -> Vec> { + let primary = EventKindDiscriminant::from(event); + let mut buckets: Vec<&Vec>> = Vec::with_capacity(2); + if let Some(v) = self.by_kind.get(&primary) { + buckets.push(v); + } + if matches!(event, EventKind::BusInvokeOf(_)) { + if let Some(v) = self.by_kind.get(&EventKindDiscriminant::BusInvoke) { + buckets.push(v); + } + } + let mut hits: Vec> = buckets.into_iter() + .flat_map(|v| v.iter()) + .filter(|r| matches_pattern(&r.when, event, history)) + .filter(|r| matches_scope(&r.scope, subject)) + .cloned() + .collect(); + // Fallback: reglas compuestas (Either/All) se evalúan siempre. + for r in &self.compound { + if matches_pattern(&r.when, event, history) && matches_scope(&r.scope, subject) { + hits.push(r.clone()); + } + } + hits.sort_by(|a, b| b.priority.cmp(&a.priority)); + hits + } +} + +/// Match recursivo del pattern. Atomic patterns evalúan contra el evento +/// actual + history. Compuestos (Either/All) recursan sobre sus children. +fn matches_pattern(pattern: &EventPattern, event: &EventKind, history: &[TimedEvent]) -> bool { + match pattern { + EventPattern::Single { kind } => matches_event_payload(kind, event), + EventPattern::Sequence { kinds, within_ms } => { + if kinds.is_empty() { return false; } + let last_kind = kinds.last().unwrap(); + if !matches_event_payload(last_kind, event) { return false; } + if history.len() < kinds.len() { return false; } + let tail = &history[history.len() - kinds.len()..]; + for (t, k) in tail.iter().zip(kinds) { + if !matches_event_payload(k, &t.kind) { return false; } + } + if *within_ms > 0 { + let span = tail.last().unwrap().at.duration_since(tail.first().unwrap().at); + if span > Duration::from_millis(*within_ms) { return false; } + } + true + } + EventPattern::Either { patterns } => { + patterns.iter().any(|p| matches_pattern(p, event, history)) + } + EventPattern::All { patterns } => { + patterns.iter().all(|p| matches_pattern(p, event, history)) + } + } +} + +fn matches_event_payload(rule_kind: &EventKind, evt: &EventKind) -> bool { + use EventKind::*; + match (rule_kind, evt) { + (EnteSpawned, EnteSpawned) => true, + (EnteDied, EnteDied) => true, + (BusAnnounce, BusAnnounce) => true, + (BusInvoke, BusInvoke) | (BusInvoke, BusInvokeOf(_)) => true, + (BusInvokeOf(want), BusInvokeOf(got)) => want == got, + (DeviceAdded, DeviceAdded) => true, + (DeviceRemoved, DeviceRemoved) => true, + (Custom(want), Custom(got)) => want == got, + _ => false, + } +} + +fn matches_scope(scope: &Scope, subj: &SubjectInfo) -> bool { + if scope.is_wildcard() { return true; } + if let Some(id) = scope.subject_id { + if subj.id != Some(id) { return false; } + } + if let Some(lbl) = &scope.subject_label { + if subj.label.as_ref() != Some(lbl) { return false; } + } + if let Some(cap) = &scope.subject_has_cap { + if !subj.capabilities.contains(cap) { return false; } + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rules::{Action, EventPattern, LogLevel}; + use std::time::{Duration, Instant}; + + fn rule_single(id_str: &str, kind: EventKind, prio: u8) -> Rule { + Rule { + id: id_str.parse().unwrap(), + priority: prio, + when: EventPattern::Single { kind }, + then: vec![Action::Log { + level: LogLevel::Info, + message: id_str.into(), + }], + scope: Scope::default(), + } + } + + fn empty_history() -> Vec { Vec::new() } + + #[test] + fn dispatch_picks_only_matching_kind() { + let mut e = RuleEngine::empty(); + e.insert(rule_single("01KQQ100000000000000000001", EventKind::EnteSpawned, 5)); + e.insert(rule_single("01KQQ100000000000000000002", EventKind::EnteDied, 5)); + let hits = e.dispatch(&EventKind::EnteSpawned, &SubjectInfo::default(), &empty_history()); + assert_eq!(hits.len(), 1); + } + + #[test] + fn priority_orders_descending() { + let mut e = RuleEngine::empty(); + e.insert(rule_single("01KQQ100000000000000000003", EventKind::EnteSpawned, 1)); + e.insert(rule_single("01KQQ100000000000000000004", EventKind::EnteSpawned, 9)); + let hits = e.dispatch(&EventKind::EnteSpawned, &SubjectInfo::default(), &empty_history()); + assert_eq!(hits[0].priority, 9); + assert_eq!(hits[1].priority, 1); + } + + #[test] + fn scope_filters_by_label() { + let mut e = RuleEngine::empty(); + let mut r = rule_single("01KQQ100000000000000000005", EventKind::EnteSpawned, 5); + r.scope = Scope { subject_label: Some("foo".into()), ..Default::default() }; + e.insert(r); + let foo = SubjectInfo { label: Some("foo".into()), ..Default::default() }; + let bar = SubjectInfo { label: Some("bar".into()), ..Default::default() }; + assert_eq!(e.dispatch(&EventKind::EnteSpawned, &foo, &empty_history()).len(), 1); + assert_eq!(e.dispatch(&EventKind::EnteSpawned, &bar, &empty_history()).len(), 0); + } + + #[test] + fn bus_invoke_generic_matches_specific() { + let mut e = RuleEngine::empty(); + e.insert(rule_single("01KQQ100000000000000000006", EventKind::BusInvoke, 5)); + let hits = e.dispatch( + &EventKind::BusInvokeOf(Capability::LegacyLogind), + &SubjectInfo::default(), + &empty_history(), + ); + assert_eq!(hits.len(), 1); + } + + #[test] + fn sequence_pattern_matches_with_history() { + let mut e = RuleEngine::empty(); + let r = Rule { + id: "01KQQ100000000000000000007".parse().unwrap(), + priority: 5, + when: EventPattern::Sequence { + kinds: vec![EventKind::EnteSpawned, EventKind::BusAnnounce], + within_ms: 1000, + }, + then: vec![Action::Log { level: LogLevel::Info, message: "seq".into() }], + scope: Scope::default(), + }; + e.insert(r); + + let now = Instant::now(); + let history = vec![ + TimedEvent { kind: EventKind::EnteSpawned, at: now }, + TimedEvent { kind: EventKind::BusAnnounce, at: now + Duration::from_millis(50) }, + ]; + let hits = e.dispatch(&EventKind::BusAnnounce, &SubjectInfo::default(), &history); + assert_eq!(hits.len(), 1, "esperaba match secuencia, got {}", hits.len()); + } + + #[test] + fn sequence_rejects_outside_time_window() { + let mut e = RuleEngine::empty(); + let r = Rule { + id: "01KQQ100000000000000000008".parse().unwrap(), + priority: 5, + when: EventPattern::Sequence { + kinds: vec![EventKind::EnteSpawned, EventKind::BusAnnounce], + within_ms: 100, + }, + then: vec![Action::Log { level: LogLevel::Info, message: "seq".into() }], + scope: Scope::default(), + }; + e.insert(r); + let now = Instant::now(); + let history = vec![ + TimedEvent { kind: EventKind::EnteSpawned, at: now }, + TimedEvent { kind: EventKind::BusAnnounce, at: now + Duration::from_millis(500) }, + ]; + let hits = e.dispatch(&EventKind::BusAnnounce, &SubjectInfo::default(), &history); + assert!(hits.is_empty(), "no debería matchear fuera de la ventana"); + } + + #[test] + fn either_matches_any_branch() { + let mut e = RuleEngine::empty(); + let r = Rule { + id: "01KQQ100000000000000000010".parse().unwrap(), + priority: 5, + when: EventPattern::Either { patterns: vec![ + EventPattern::Single { kind: EventKind::EnteSpawned }, + EventPattern::Single { kind: EventKind::EnteDied }, + ]}, + then: vec![Action::Log { level: LogLevel::Info, message: "either".into() }], + scope: Scope::default(), + }; + e.insert(r); + assert_eq!(e.dispatch(&EventKind::EnteSpawned, &SubjectInfo::default(), &[]).len(), 1); + assert_eq!(e.dispatch(&EventKind::EnteDied, &SubjectInfo::default(), &[]).len(), 1); + assert_eq!(e.dispatch(&EventKind::BusAnnounce, &SubjectInfo::default(), &[]).len(), 0); + } + + #[test] + fn all_requires_every_branch() { + let mut e = RuleEngine::empty(); + // All: matchear sólo si el evento actual es BusAnnounce Y la + // secuencia EnteSpawned→BusAnnounce ocurrió en history. + let r = Rule { + id: "01KQQ100000000000000000011".parse().unwrap(), + priority: 5, + when: EventPattern::All { patterns: vec![ + EventPattern::Single { kind: EventKind::BusAnnounce }, + EventPattern::Sequence { + kinds: vec![EventKind::EnteSpawned, EventKind::BusAnnounce], + within_ms: 0, + }, + ]}, + then: vec![Action::Log { level: LogLevel::Info, message: "all".into() }], + scope: Scope::default(), + }; + e.insert(r); + + let now = Instant::now(); + let history = vec![ + TimedEvent { kind: EventKind::EnteSpawned, at: now }, + TimedEvent { kind: EventKind::BusAnnounce, at: now + Duration::from_millis(10) }, + ]; + // Single y Sequence ambos matchean → All matches. + assert_eq!(e.dispatch(&EventKind::BusAnnounce, &SubjectInfo::default(), &history).len(), 1); + // Sólo Single matchea (history vacío) → All no matches. + assert!(e.dispatch(&EventKind::BusAnnounce, &SubjectInfo::default(), &[]).is_empty()); + } + + #[test] + fn sequence_requires_correct_order() { + let mut e = RuleEngine::empty(); + let r = Rule { + id: "01KQQ100000000000000000009".parse().unwrap(), + priority: 5, + when: EventPattern::Sequence { + kinds: vec![EventKind::EnteSpawned, EventKind::BusAnnounce], + within_ms: 0, + }, + then: vec![Action::Log { level: LogLevel::Info, message: "seq".into() }], + scope: Scope::default(), + }; + e.insert(r); + let now = Instant::now(); + // Orden invertido en el history. + let history = vec![ + TimedEvent { kind: EventKind::BusAnnounce, at: now }, + TimedEvent { kind: EventKind::EnteSpawned, at: now + Duration::from_millis(10) }, + ]; + // El evento actual es EnteSpawned, pero el último de la secuencia + // requerida es BusAnnounce — no debería matchear. + let hits = e.dispatch(&EventKind::EnteSpawned, &SubjectInfo::default(), &history); + assert!(hits.is_empty()); + } +} diff --git a/crates/core/ente-brain/src/introspect.rs b/crates/core/ente-brain/src/introspect.rs new file mode 100644 index 0000000..77972d0 --- /dev/null +++ b/crates/core/ente-brain/src/introspect.rs @@ -0,0 +1,476 @@ +//! Introspect API. Unix Domain Socket + framing length-prefijo + bincode. +//! +//! Una herramienta externa (ej. `brainctl`) puede consultar el estado del +//! cerebro sin tocar el bus interno del fractal. Esto separa observación de +//! ejecución — la introspección es read-only por diseño. + +use crate::crystallize::{detect_crystals, Crystal, CrystallizationParams}; +use crate::engine::RuleEngine; +use crate::observer::Observer; +use crate::rules::Rule; +use serde::{Deserialize, Serialize}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::RwLock; +use tracing::{debug, info, trace, warn}; +use ulid::Ulid; + +const MAX_FRAME: usize = 4 * 1024 * 1024; // 4 MiB — correlation matrices crecen + +/// Estado compartido entre el bucle del Init y el servidor de introspección. +/// `Arc>` permite muchos lectores concurrentes (introspect) y +/// un escritor (el dispatcher de eventos en el bucle primordial). +#[derive(Clone)] +pub struct BrainState { + pub engine: Arc>, + pub observer: Arc>, + pub params: CrystallizationParams, + /// Path opcional donde apendear reglas promovidas en JSONL. Si Some, + /// cada PromoteCrystal añade una línea (append-only) con la Rule serializada. + pub rules_out: Option>, + /// Audit log en memoria. Cada promote/remove deja huella aquí. + pub audit: Arc>, +} + +impl BrainState { + pub fn new(window_size: usize) -> Self { + Self::with_params(window_size, CrystallizationParams::default()) + } + + pub fn with_params(window_size: usize, params: CrystallizationParams) -> Self { + Self { + engine: Arc::new(RwLock::new(RuleEngine::empty())), + observer: Arc::new(RwLock::new(Observer::new(window_size))), + params, + rules_out: None, + audit: Arc::new(RwLock::new(crate::audit::AuditLog::new())), + } + } + + pub fn with_rules_out(mut self, path: PathBuf) -> Self { + self.rules_out = Some(Arc::new(path)); + self + } +} + +/// Append-only writer de una `Rule` serializada a `rules_out` en formato +/// JSONL: una línea = un Rule JSON. Idempotente respecto a re-flushes +/// porque el caller se encarga de no apendar la misma rule dos veces. +/// El loader (`loader::extract_rules_from_json`) acepta tanto JSONL como +/// arrays — el archivo es legible en ambos modos. +pub fn append_rule_jsonl(path: &Path, rule: &Rule) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path)?; + let line = serde_json::to_string(rule) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + writeln!(file, "{line}")?; + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum IntrospectRequest { + /// Lista resumida de reglas vivas. + ListRules, + /// Detalle de una regla concreta. + GetRule(Ulid), + /// Snapshot de la entropía y conteos básicos. + EntropySnapshot, + /// Top N pares (a, b) por co-ocurrencia. + TopCorrelations { n: usize }, + /// Cristales detectados con los parámetros del BrainState. + Crystals, + /// Serializa la Rule derivada de un cristal específico como JSON + /// (índice tras Crystals). + CrystalJson { index: usize }, + /// Promueve el cristal #index a regla viva en el motor. Devuelve el + /// rule_id asignado y el JSON de la Rule para auditoría/persistencia. + PromoteCrystal { index: usize }, + /// Elimina una regla viva por id. Útil para revertir un promote. + RemoveRule { id: Ulid }, + /// Lista las últimas N entradas del audit log. limit=0 = todas. + ListAudit { limit: usize }, + /// Persiste todas las entries pendientes al CAS y actualiza el head + /// pointer si el log lo tiene configurado. + FlushAudit, + /// Recarga reglas desde el archivo configurado por --rules-out (o el + /// path provisto). Vacía el engine antes de cargar. + ReloadRules { path: Option }, + /// Verifica la cadena audit recorriendo prev_sha hasta el genesis, + /// validando integridad de cada entry contra el CAS. + VerifyAudit, + /// Reconstruye el engine desde la cadena audit. Vacía engine y aplica + /// PromoteCrystal/RemoveRule en orden cronológico. + ReplayAudit, + /// Mantiene la conexión abierta y empuja cada `AuditEntry` nuevo en + /// frames `IntrospectResponse::AuditStreamFrame` hasta que el cliente + /// cierra. Tras esta request no se aceptan más requests en la misma conn. + StreamAudit, + /// Garbage-collect el CAS. Considera reachable: todo lo alcanzable desde + /// el head del audit log. Cualquier blob extra (Wasm modules referenciados + /// por Cards) debe haberse pasado en `extra_roots` por el caller. + GcCas { extra_roots: Vec<[u8; 32]> }, + /// Detecta cristales de patrones temporales (Burst, Silence). + PatternCrystals, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum IntrospectResponse { + Rules(Vec), + Rule(Option), + Entropy { value_bits: f64, sample_size: u64, distinct_kinds: usize, window_full: bool }, + Correlations(Vec), + Crystals(Vec), + Json(String), + /// Resultado de PromoteCrystal: id de la regla creada + JSON de la Rule + /// para que el operador lo persista en disco si quiere. + Promoted { rule_id: Ulid, rule_json: String }, + /// Resultado de RemoveRule: true si existía, false si ya no. + Removed(bool), + /// Entradas del audit log (más recientes al final). + AuditEntries(Vec), + /// Resultado de FlushAudit: cuántas entries se escribieron y SHA del head. + Flushed { written: usize, head_sha: Option<[u8; 32]>, total_flushed: u64 }, + /// Resultado de ReloadRules: número total de reglas tras el reload. + Reloaded { count: usize }, + /// Resultado de VerifyAudit. + AuditVerified(crate::audit::VerificationReport), + /// Resultado de ReplayAudit. + Replayed(crate::audit::ReplayReport), + /// Frame de streaming. El cliente lee estos en bucle hasta EOF. + AuditStreamFrame(crate::audit::AuditEntry), + /// Resultado de GcCas: cuántos blobs eliminados y bytes liberados. + GcResult { deleted: usize, freed_bytes: u64 }, + /// Cristales de Burst/Silence detectados. + Patterns(Vec), + Error(String), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RuleSummary { + pub id: Ulid, + pub priority: u8, + pub event_kind_tag: String, + pub action_count: usize, + pub scope_wildcard: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CorrelationEntry { + pub a: String, + pub b: String, + pub joint_count: u64, + pub conditional_prob: f64, + pub pmi_bits: f64, +} + +pub struct IntrospectServer { + state: BrainState, +} + +impl IntrospectServer { + pub fn new(state: BrainState) -> Self { Self { state } } + + /// Spawn del listener. Devuelve cuando bind() falla; en caso contrario + /// corre indefinidamente. + pub async fn serve(self, path: &Path) -> anyhow::Result<()> { + let _ = std::fs::remove_file(path); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let listener = UnixListener::bind(path)?; + info!(path = %path.display(), "brain introspect escuchando"); + let arc_self = Arc::new(self); + loop { + match listener.accept().await { + Ok((stream, _)) => { + trace!("introspect conn aceptada"); + let me = arc_self.clone(); + tokio::spawn(async move { + if let Err(e) = me.handle(stream).await { + warn!(?e, "introspect conn ended"); + } + }); + } + Err(e) => { + warn!(?e, "introspect accept failed"); + return Ok(()); + } + } + } + } + + async fn handle(self: Arc, mut stream: UnixStream) -> anyhow::Result<()> { + loop { + let mut len_buf = [0u8; 4]; + if stream.read_exact(&mut len_buf).await.is_err() { + return Ok(()); // EOF + } + let len = u32::from_be_bytes(len_buf) as usize; + if len > MAX_FRAME { + anyhow::bail!("frame oversize: {len}"); + } + let mut buf = vec![0u8; len]; + stream.read_exact(&mut buf).await?; + let req: IntrospectRequest = bincode::deserialize(&buf)?; + debug!(?req, "introspect request"); + + // StreamAudit toma posesión de la conn — no más requests aquí. + if matches!(req, IntrospectRequest::StreamAudit) { + return self.stream_audit(stream).await; + } + + let resp = self.dispatch(req).await; + + let out = bincode::serialize(&resp)?; + stream.write_u32(out.len() as u32).await?; + stream.write_all(&out).await?; + } + } + + /// Modo streaming: subscribe al audit log y empuja cada entry como + /// frame `AuditStreamFrame`. La función retorna cuando el cliente + /// cierra (write falla) o el subscriber se desconecta. + async fn stream_audit(self: Arc, mut stream: UnixStream) -> anyhow::Result<()> { + let mut rx = self.state.audit.write().await.subscribe(); + info!("audit stream client conectado"); + while let Some(entry) = rx.recv().await { + let frame = IntrospectResponse::AuditStreamFrame(entry); + let bytes = bincode::serialize(&frame)?; + if stream.write_u32(bytes.len() as u32).await.is_err() { break; } + if stream.write_all(&bytes).await.is_err() { break; } + } + info!("audit stream client desconectado"); + Ok(()) + } + + async fn dispatch(&self, req: IntrospectRequest) -> IntrospectResponse { + match req { + IntrospectRequest::ListRules => { + let engine = self.state.engine.read().await; + let rules = engine.rules() + .map(|r| RuleSummary { + id: r.id, + priority: r.priority, + event_kind_tag: format!("{:?}", r.when), + action_count: r.then.len(), + scope_wildcard: r.scope.is_wildcard(), + }) + .collect(); + IntrospectResponse::Rules(rules) + } + IntrospectRequest::GetRule(id) => { + let engine = self.state.engine.read().await; + let found = engine.rules() + .find(|r| r.id == id) + .map(|r| Rule::clone(r)); + IntrospectResponse::Rule(found) + } + IntrospectRequest::EntropySnapshot => { + let obs = self.state.observer.read().await; + IntrospectResponse::Entropy { + value_bits: obs.shannon_entropy(), + sample_size: obs.total(), + distinct_kinds: obs.marginals().len(), + window_full: obs.current_window() >= obs.window_size(), + } + } + IntrospectRequest::TopCorrelations { n } => { + let obs = self.state.observer.read().await; + let mut entries: Vec = obs.cooccurrences().iter() + .map(|((a, b), &joint)| CorrelationEntry { + a: format!("{:?}", a), + b: format!("{:?}", b), + joint_count: joint, + conditional_prob: obs.conditional_prob(a, b), + pmi_bits: obs.pmi(a, b), + }) + .collect(); + entries.sort_by(|x, y| y.joint_count.cmp(&x.joint_count)); + entries.truncate(n); + IntrospectResponse::Correlations(entries) + } + IntrospectRequest::Crystals => { + let obs = self.state.observer.read().await; + let crystals = detect_crystals(&obs, &self.state.params); + IntrospectResponse::Crystals(crystals) + } + IntrospectRequest::CrystalJson { index } => { + let obs = self.state.observer.read().await; + let crystals = detect_crystals(&obs, &self.state.params); + match crystals.get(index) { + Some(c) => IntrospectResponse::Json(crate::crystallize::crystal_to_json_pretty(c)), + None => IntrospectResponse::Error(format!("no crystal at index {index}")), + } + } + IntrospectRequest::PromoteCrystal { index } => { + let crystals = { + let obs = self.state.observer.read().await; + detect_crystals(&obs, &self.state.params) + }; + match crystals.get(index) { + Some(c) => { + let rule = crate::crystallize::crystal_to_rule(c); + let rule_id = rule.id; + let rule_json = serde_json::to_string_pretty(&rule) + .unwrap_or_else(|_| "".into()); + self.state.engine.write().await.insert(rule.clone()); + // Persistencia opcional al archivo JSONL. + if let Some(path) = self.state.rules_out.as_ref() { + if let Err(e) = append_rule_jsonl(path, &rule) { + warn!(?e, path = %path.display(), "rules_out append falló"); + } else { + info!(path = %path.display(), %rule_id, "regla persistida a JSONL"); + } + } + // Audit entry + self.state.audit.write().await.append( + crate::audit::AuditAction::PromoteCrystal { + rule_id, crystal: c.clone(), + } + ); + IntrospectResponse::Promoted { rule_id, rule_json } + } + None => IntrospectResponse::Error(format!("no crystal at index {index}")), + } + } + IntrospectRequest::RemoveRule { id } => { + let removed = self.state.engine.write().await.remove(id); + if removed { + self.state.audit.write().await.append( + crate::audit::AuditAction::RemoveRule { rule_id: id } + ); + } + IntrospectResponse::Removed(removed) + } + IntrospectRequest::ListAudit { limit } => { + let audit = self.state.audit.read().await; + IntrospectResponse::AuditEntries(audit.recent(limit).cloned().collect()) + } + IntrospectRequest::FlushAudit => { + let mut audit = self.state.audit.write().await; + match audit.flush_to_cas() { + Ok(written) => IntrospectResponse::Flushed { + written, + head_sha: audit.last_flushed_sha(), + total_flushed: audit.flushed_count(), + }, + Err(e) => IntrospectResponse::Error(format!("flush_to_cas: {e}")), + } + } + IntrospectRequest::VerifyAudit => { + let head = self.state.audit.read().await.last_flushed_sha(); + let head = match head { + Some(h) => h, + None => return IntrospectResponse::Error( + "audit log sin entries flushadas — nada que verificar".into() + ), + }; + let report = crate::audit::verify_chain_from_cas(head); + IntrospectResponse::AuditVerified(report) + } + IntrospectRequest::StreamAudit => { + // Inalcanzable por construcción: handle() detecta StreamAudit + // antes de llamar a dispatch(). Pero el match exige cubrir. + IntrospectResponse::Error( + "StreamAudit no debe llegar a dispatch — bug del handler".into() + ) + } + IntrospectRequest::PatternCrystals => { + let obs = self.state.observer.read().await; + let params = crate::crystallize::PatternParams::default(); + let patterns = crate::crystallize::detect_pattern_crystals(&obs, ¶ms); + IntrospectResponse::Patterns(patterns) + } + IntrospectRequest::GcCas { extra_roots } => { + // Reachable = audit chain desde head + extra_roots provistos. + let mut reachable = std::collections::HashSet::new(); + if let Some(head) = self.state.audit.read().await.last_flushed_sha() { + reachable.extend(crate::audit::reachable_from_head(head)); + } + reachable.extend(extra_roots); + match ente_cas::gc(&reachable) { + Ok((deleted, freed_bytes)) => IntrospectResponse::GcResult { deleted, freed_bytes }, + Err(e) => IntrospectResponse::Error(format!("gc: {e}")), + } + } + IntrospectRequest::ReplayAudit => { + let head = self.state.audit.read().await.last_flushed_sha(); + let head = match head { + Some(h) => h, + None => return IntrospectResponse::Error( + "audit log sin entries flushadas — nada que replayar".into() + ), + }; + let mut engine = self.state.engine.write().await; + *engine = crate::engine::RuleEngine::empty(); + let report = crate::audit::replay_chain(head, &mut engine); + IntrospectResponse::Replayed(report) + } + IntrospectRequest::ReloadRules { path } => { + // Path explícito gana sobre el rules_out configurado. + let resolved = path.map(std::path::PathBuf::from) + .or_else(|| self.state.rules_out.as_ref().map(|p| p.as_path().to_path_buf())); + let path = match resolved { + Some(p) => p, + None => return IntrospectResponse::Error( + "ReloadRules sin path y sin rules_out configurado".into() + ), + }; + let rules = match crate::loader::load_rules_file(&path) { + Ok(r) => r, + Err(e) => return IntrospectResponse::Error(format!("load: {e}")), + }; + // Vaciamos el engine antes de re-cargar — semántica clean-slate. + let mut engine = self.state.engine.write().await; + *engine = crate::engine::RuleEngine::empty(); + let count = rules.len(); + for r in rules { engine.insert(r); } + drop(engine); + self.state.audit.write().await.append( + crate::audit::AuditAction::LoadRulesFile { + path: path.to_string_lossy().into_owned(), + count, + } + ); + IntrospectResponse::Reloaded { count } + } + } + } +} + +// Cliente helper para tools externos (brainctl). +pub async fn call(path: &Path, req: IntrospectRequest) -> anyhow::Result { + let mut stream = UnixStream::connect(path).await?; + let buf = bincode::serialize(&req)?; + stream.write_u32(buf.len() as u32).await?; + stream.write_all(&buf).await?; + + let mut len_buf = [0u8; 4]; + stream.read_exact(&mut len_buf).await?; + let len = u32::from_be_bytes(len_buf) as usize; + if len > MAX_FRAME { + anyhow::bail!("response oversize: {len}"); + } + let mut buf = vec![0u8; len]; + stream.read_exact(&mut buf).await?; + Ok(bincode::deserialize(&buf)?) +} + +/// Consume la lista marginal del observer para humanos. Suprime el detalle +/// crudo de `EventKind` (ej. payloads largos en BusInvokeOf). +pub fn marginal_summary(obs: &Observer) -> Vec<(String, u64)> { + let mut entries: Vec<(String, u64)> = obs.marginals().iter() + .map(|(k, &c)| (format!("{:?}", k), c)) + .collect(); + entries.sort_by(|x, y| y.1.cmp(&x.1)); + entries +} + diff --git a/crates/core/ente-brain/src/lib.rs b/crates/core/ente-brain/src/lib.rs new file mode 100644 index 0000000..49ac4eb --- /dev/null +++ b/crates/core/ente-brain/src/lib.rs @@ -0,0 +1,38 @@ +//! ente-brain: motor de reglas determinista + observador estadístico. +//! +//! Tres capas: +//! 1. `rules` — tipos de regla (Triplet: Subject + Event + Action) +//! 2. `engine` — RuleEngine con HashMap>> +//! para dispatch O(1) +//! 3. `dispatch` — ejecutor async de Actions (vía tokio) +//! 4. `observer` — sliding window + marginales + co-ocurrencias +//! + Shannon entropy + información mutua +//! 5. `crystallize` — detección de patrones estadísticamente significativos +//! y materialización en `Rule` ejecutables +//! 6. `introspect` — Unix socket bincode API para tools externos +//! +//! Diseño de inmutabilidad: +//! - Rules son `Arc` — clonar es zero-copy (refcount bump). +//! - El motor expone sólo lecturas; mutaciones pasan por `insert/remove`. +//! - Observer mantiene contadores incrementales — sin recomputación. + +pub mod audit; +pub mod autopromote; +pub mod crystallize; +pub mod dispatch; +pub mod engine; +pub mod introspect; +pub mod loader; +pub mod metrics; +pub mod observer; +pub mod rules; + +pub use autopromote::{spawn_autopromote_loop, AutopromoteParams}; +pub use crystallize::{detect_crystals, Crystal, CrystallizationParams}; +pub use dispatch::{dispatch_actions, ActionSink, NullSink}; +pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo}; +pub use introspect::{IntrospectRequest, IntrospectResponse, IntrospectServer, BrainState}; +pub use loader::{load_card_file, load_rules_file}; +pub use metrics::serve_metrics; +pub use observer::{Observer, TimedEvent}; +pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope}; diff --git a/crates/core/ente-brain/src/loader.rs b/crates/core/ente-brain/src/loader.rs new file mode 100644 index 0000000..296ebc1 --- /dev/null +++ b/crates/core/ente-brain/src/loader.rs @@ -0,0 +1,172 @@ +//! Loader de Cards y Reglas desde archivos JSON. +//! +//! Sustituye al antiguo `kcl_loader.rs` (eliminado): la rama KCL invocaba +//! un subprocess al CLI Go `kcl` que ningún target real tenía instalado y +//! cuya validación duplicaba `EntityCard::validate()`. La fuente de verdad +//! del shape de la Card es Rust + serde; en disco se guarda JSON crudo. +//! +//! Ergonomía de autoría futura (RON, Dhall, etc.) se añade como ramas +//! adicionales aquí cuando duela escribir JSON a mano. Hoy: una sola rama. + +use crate::rules::Rule; +use ente_card::EntityCard; +use std::path::Path; +use tracing::info; + +/// Carga una `EntityCard` desde un archivo JSON. Pasa por +/// `EntityCard::validate()` antes de devolver — falla rápida. +pub fn load_card_file(path: &Path) -> anyhow::Result { + info!(path = %path.display(), "cargando Card desde JSON"); + let raw = std::fs::read_to_string(path)?; + let card = extract_card_from_json(&raw)?; + card.validate() + .map_err(|e| anyhow::anyhow!("Card inválida ({}): {e}", path.display()))?; + Ok(card) +} + +/// Extrae una `EntityCard` de JSON. Acepta: +/// 1. Object directamente serializable como EntityCard. +/// 2. Object dict con un único valor que sea EntityCard (compat con +/// salidas de generadores que envuelven en `{"seed": {...}}`). +pub fn extract_card_from_json(raw: &str) -> anyhow::Result { + let v: serde_json::Value = serde_json::from_str(raw)?; + let direct_err = match serde_json::from_value::(v.clone()) { + Ok(c) => return Ok(c), + Err(e) => e, + }; + if let serde_json::Value::Object(map) = v { + for (_, vv) in map { + if let Ok(c) = serde_json::from_value::(vv) { + return Ok(c); + } + } + } + // Propagamos el error del intento directo: es el caso típico (JSON top-level + // = EntityCard) y su mensaje apunta al campo concreto que rompió. + anyhow::bail!("JSON no contiene una EntityCard válida: {direct_err}") +} + +/// Carga reglas desde un archivo JSON. +pub fn load_rules_file(path: &Path) -> anyhow::Result> { + info!(path = %path.display(), "cargando reglas desde JSON"); + let raw = std::fs::read_to_string(path)?; + extract_rules_from_json(&raw) +} + +/// Extrae un `Vec` de un blob de texto. Acepta tres formas: +/// 1. JSONL: una `Rule` por línea (el formato que escribe `append_rule_jsonl`). +/// 2. Array directo: `[{...}, {...}]`. +/// 3. Object con un campo array: `{"rules": [...]}`. +/// +/// Heurística: si el primer carácter no-blanco es `[` o `{` con formato +/// "objeto-con-array", parseamos como JSON único; en otro caso intentamos +/// línea-por-línea. Líneas vacías o que empiecen con `#` se ignoran (compat +/// con archivos editados a mano que dejen comentarios estilo shell). +pub fn extract_rules_from_json(raw: &str) -> anyhow::Result> { + let trimmed_start = raw.trim_start(); + let looks_jsonl = trimmed_start.starts_with('{') + && raw.lines().filter(|l| { + let t = l.trim(); + !t.is_empty() && !t.starts_with('#') + }).count() > 1; + + if !looks_jsonl { + // Camino clásico: un único documento JSON (array o objeto). + if let Ok(v) = serde_json::from_str::(raw) { + let arr = match v { + serde_json::Value::Array(_) => v, + serde_json::Value::Object(map) => map + .into_values() + .find(|x| x.is_array()) + .ok_or_else(|| anyhow::anyhow!("JSON no contiene ningún array"))?, + _ => anyhow::bail!("JSON debe ser array o object con campo array"), + }; + return Ok(serde_json::from_value(arr)?); + } + // Caer a JSONL si el documento único no parsea — útil para archivos + // que mezclan comentarios `#` (no JSON válido como documento único). + } + + let mut rules = Vec::new(); + for (idx, line) in raw.lines().enumerate() { + let t = line.trim(); + if t.is_empty() || t.starts_with('#') { continue; } + let rule: Rule = serde_json::from_str(t) + .map_err(|e| anyhow::anyhow!("JSONL línea {}: {e}", idx + 1))?; + rules.push(rule); + } + Ok(rules) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::introspect::append_rule_jsonl; + use crate::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope}; + use ulid::Ulid; + + fn sample_rule() -> Rule { + Rule { + id: Ulid::new(), + priority: 5, + when: EventPattern::Single { kind: EventKind::EnteSpawned }, + then: vec![Action::Log { + level: LogLevel::Info, + message: "test".into(), + }], + scope: Scope::default(), + } + } + + #[test] + fn rules_from_array() { + let r = sample_rule(); + let raw = format!("[{}]", serde_json::to_string(&r).unwrap()); + let parsed = extract_rules_from_json(&raw).expect("array parse"); + assert_eq!(parsed.len(), 1); + } + + #[test] + fn rules_from_object_with_array() { + let r = sample_rule(); + let raw = format!(r#"{{"rules":[{}]}}"#, serde_json::to_string(&r).unwrap()); + let parsed = extract_rules_from_json(&raw).expect("object parse"); + assert_eq!(parsed.len(), 1); + } + + #[test] + fn rules_from_jsonl_with_comments_and_blanks() { + let r1 = sample_rule(); + let r2 = sample_rule(); + let raw = format!( + "# header comment\n\n{}\n# inline comment\n{}\n\n", + serde_json::to_string(&r1).unwrap(), + serde_json::to_string(&r2).unwrap() + ); + let parsed = extract_rules_from_json(&raw).expect("jsonl parse"); + assert_eq!(parsed.len(), 2); + } + + #[test] + fn append_rule_jsonl_roundtrip() { + let dir = tempdir_unique(); + let path = dir.join("rules.jsonl"); + let r1 = sample_rule(); + let r2 = sample_rule(); + append_rule_jsonl(&path, &r1).expect("append 1"); + append_rule_jsonl(&path, &r2).expect("append 2"); + let raw = std::fs::read_to_string(&path).expect("read back"); + let parsed = extract_rules_from_json(&raw).expect("roundtrip parse"); + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0].id, r1.id); + assert_eq!(parsed[1].id, r2.id); + let _ = std::fs::remove_dir_all(&dir); + } + + fn tempdir_unique() -> std::path::PathBuf { + let base = std::env::temp_dir(); + let p = base.join(format!("ente-brain-loader-{}", Ulid::new())); + std::fs::create_dir_all(&p).unwrap(); + p + } +} diff --git a/crates/core/ente-brain/src/metrics.rs b/crates/core/ente-brain/src/metrics.rs new file mode 100644 index 0000000..ad44be2 --- /dev/null +++ b/crates/core/ente-brain/src/metrics.rs @@ -0,0 +1,175 @@ +//! Endpoint Prometheus en TCP. Formato text/plain (exposition format 0.0.4). +//! +//! Sin dependencias adicionales — la cardinalidad de nuestras métricas es +//! pequeña y el formato es trivial. Si crece, sustituir por la crate +//! `prometheus` con su Registry + encoders. + +use crate::introspect::BrainState; +use crate::rules::EventKind; +use std::net::SocketAddr; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tracing::{info, trace, warn}; + +/// Lanza el listener Prometheus. Devuelve cuando bind() falla; en caso +/// contrario corre indefinidamente. Pensado para `tokio::spawn`. +pub async fn serve_metrics(state: BrainState, addr: SocketAddr) -> anyhow::Result<()> { + let listener = TcpListener::bind(addr).await?; + info!(?addr, "prometheus /metrics escuchando"); + loop { + match listener.accept().await { + Ok((stream, peer)) => { + trace!(?peer, "metrics scrape"); + let s = state.clone(); + tokio::spawn(async move { + if let Err(e) = handle_scrape(stream, s).await { + warn!(?e, "metrics conn ended"); + } + }); + } + Err(e) => { + warn!(?e, "metrics accept failed"); + return Ok(()); + } + } + } +} + +async fn handle_scrape(mut stream: TcpStream, state: BrainState) -> anyhow::Result<()> { + // Drenamos el request line + headers sin parsear (cualquier path + // responde igual — Prometheus envía GET /metrics típicamente). + let mut buf = [0u8; 1024]; + let _ = stream.read(&mut buf).await; + let body = format_metrics(&state).await; + let resp = format!( + "HTTP/1.1 200 OK\r\n\ + Content-Type: text/plain; version=0.0.4\r\n\ + Content-Length: {}\r\n\ + Connection: close\r\n\ + \r\n\ + {}", + body.len(), body + ); + stream.write_all(resp.as_bytes()).await?; + stream.shutdown().await?; + Ok(()) +} + +async fn format_metrics(state: &BrainState) -> String { + let obs = state.observer.read().await; + let engine = state.engine.read().await; + let audit = state.audit.read().await; + + let mut out = String::with_capacity(2048); + + // ---- Entropía ---- + out.push_str("# HELP ente_brain_entropy_bits Shannon entropy of marginal event distribution.\n"); + out.push_str("# TYPE ente_brain_entropy_bits gauge\n"); + out.push_str(&format!("ente_brain_entropy_bits {:.6}\n", obs.shannon_entropy())); + + // ---- Tamaño de muestra ---- + out.push_str("# HELP ente_brain_events_total Total events recorded by the observer.\n"); + out.push_str("# TYPE ente_brain_events_total counter\n"); + out.push_str(&format!("ente_brain_events_total {}\n", obs.total())); + + // ---- Distinct kinds ---- + out.push_str("# HELP ente_brain_distinct_kinds Number of distinct EventKind tags seen.\n"); + out.push_str("# TYPE ente_brain_distinct_kinds gauge\n"); + out.push_str(&format!("ente_brain_distinct_kinds {}\n", obs.marginals().len())); + + // ---- Window ocupación ---- + out.push_str("# HELP ente_brain_window_size Current sliding window length.\n"); + out.push_str("# TYPE ente_brain_window_size gauge\n"); + out.push_str(&format!("ente_brain_window_size {}\n", obs.current_window())); + + // ---- Reglas vivas ---- + out.push_str("# HELP ente_brain_rules_active Number of rules currently in the engine.\n"); + out.push_str("# TYPE ente_brain_rules_active gauge\n"); + out.push_str(&format!("ente_brain_rules_active {}\n", engine.len())); + + // ---- Eventos por kind ---- + out.push_str("# HELP ente_brain_events_by_kind Events by EventKind tag.\n"); + out.push_str("# TYPE ente_brain_events_by_kind counter\n"); + for (k, c) in obs.marginals() { + out.push_str(&format!( + "ente_brain_events_by_kind{{kind=\"{}\"}} {}\n", + kind_label(k), c + )); + } + + // ---- Cristales detectados (con params actuales) ---- + let crystals = crate::detect_crystals(&obs, &state.params); + out.push_str("# HELP ente_brain_crystals_total Number of crystals detected with current params.\n"); + out.push_str("# TYPE ente_brain_crystals_total gauge\n"); + out.push_str(&format!("ente_brain_crystals_total {}\n", crystals.len())); + + // ---- Audit log ---- + out.push_str("# HELP ente_brain_audit_chain_length Total entries persisted to CAS.\n"); + out.push_str("# TYPE ente_brain_audit_chain_length counter\n"); + out.push_str(&format!("ente_brain_audit_chain_length {}\n", audit.flushed_count())); + + out.push_str("# HELP ente_brain_audit_in_memory Entries currently in the in-memory ring.\n"); + out.push_str("# TYPE ente_brain_audit_in_memory gauge\n"); + out.push_str(&format!("ente_brain_audit_in_memory {}\n", audit.len())); + + out.push_str("# HELP ente_brain_audit_subscribers Active stream-audit subscribers.\n"); + out.push_str("# TYPE ente_brain_audit_subscribers gauge\n"); + out.push_str(&format!("ente_brain_audit_subscribers {}\n", audit.subscriber_count())); + + if let Some(age) = audit.last_flush_age_secs() { + out.push_str("# HELP ente_brain_audit_last_flush_age_seconds Time since last flush to CAS.\n"); + out.push_str("# TYPE ente_brain_audit_last_flush_age_seconds gauge\n"); + out.push_str(&format!("ente_brain_audit_last_flush_age_seconds {:.3}\n", age)); + } + if let Some(sha) = audit.last_flushed_sha() { + // Info-style metric con head sha como label. Útil para dashboards + // que quieran mostrar "current head". + out.push_str("# HELP ente_brain_audit_head_info Current head SHA of the audit chain.\n"); + out.push_str("# TYPE ente_brain_audit_head_info gauge\n"); + out.push_str(&format!( + "ente_brain_audit_head_info{{sha=\"{}\"}} 1\n", + ente_cas::hex(&sha) + )); + } + + // ---- Histogramas de gaps temporales (top-32 pares más frecuentes) ---- + out.push_str("# HELP ente_brain_pair_gap_seconds Time gap between correlated events.\n"); + out.push_str("# TYPE ente_brain_pair_gap_seconds histogram\n"); + let limits = crate::observer::GapHistogram::bucket_limits(); + for ((a, b), hist) in obs.top_gap_pairs(32) { + let labels = format!(r#"a="{}",b="{}""#, kind_label(a), kind_label(b)); + for (i, &limit) in limits.iter().enumerate() { + out.push_str(&format!( + "ente_brain_pair_gap_seconds_bucket{{{},le=\"{}\"}} {}\n", + labels, limit, hist.buckets[i] + )); + } + out.push_str(&format!( + "ente_brain_pair_gap_seconds_bucket{{{},le=\"+Inf\"}} {}\n", + labels, hist.count + )); + out.push_str(&format!( + "ente_brain_pair_gap_seconds_sum{{{}}} {:.6}\n", + labels, hist.sum_secs + )); + out.push_str(&format!( + "ente_brain_pair_gap_seconds_count{{{}}} {}\n", + labels, hist.count + )); + } + + out +} + +fn kind_label(k: &EventKind) -> &'static str { + match k { + EventKind::EnteSpawned => "EnteSpawned", + EventKind::EnteDied => "EnteDied", + EventKind::BusAnnounce => "BusAnnounce", + EventKind::BusInvoke => "BusInvoke", + EventKind::BusInvokeOf(_) => "BusInvokeOf", + EventKind::DeviceAdded => "DeviceAdded", + EventKind::DeviceRemoved => "DeviceRemoved", + EventKind::Custom(_) => "Custom", + } +} diff --git a/crates/core/ente-brain/src/observer.rs b/crates/core/ente-brain/src/observer.rs new file mode 100644 index 0000000..2dbf7b5 --- /dev/null +++ b/crates/core/ente-brain/src/observer.rs @@ -0,0 +1,453 @@ +//! Observador estadístico. Mantiene marginales y co-ocurrencias dentro de una +//! ventana deslizante. Calcula entropía de Shannon e información mutua para +//! identificar correlaciones significativas. +//! +//! Diseño: +//! - Counters incrementales: cada `record()` es O(window_size) en el peor +//! caso (actualiza co-ocurrencias con cada evento del window). +//! - Sin recomputaciones globales: marginales y joint counts son state. +//! - El cálculo de H(X), P(B|A), I(A;B) es O(|distinct events|). + +use crate::rules::EventKind; +use std::collections::{HashMap, VecDeque}; +use std::time::Instant; + +/// Evento timestamped. El timestamp se conserva para futuras políticas de +/// expiración por tiempo (no sólo por count). +#[derive(Debug, Clone)] +pub struct TimedEvent { + pub kind: EventKind, + pub at: Instant, +} + +/// Histograma de gaps temporales con buckets exponenciales en segundos. +/// Cubre 6 órdenes de magnitud: 1ms hasta 1000s. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct GapHistogram { + /// Buckets cumulativos (Prometheus-style): cada índice cuenta eventos + /// con gap ≤ ese límite. Limites: 1ms, 10ms, 100ms, 1s, 10s, 100s, 1000s. + pub buckets: [u64; 7], + pub count: u64, + pub sum_secs: f64, + /// Suma de cuadrados — permite calcular varianza/stddev en O(1). + pub sum_squares_secs: f64, + pub max_secs: f64, +} + +/// Estadísticas resumidas de un GapHistogram, usables en cristales temporales. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GapStats { + pub count: u64, + pub mean_secs: f64, + pub stddev_secs: f64, + pub max_secs: f64, +} + +const GAP_BUCKET_LIMITS_SECS: [f64; 7] = [ + 0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0, +]; + +impl GapHistogram { + pub fn observe(&mut self, gap_secs: f64) { + for (i, &limit) in GAP_BUCKET_LIMITS_SECS.iter().enumerate() { + if gap_secs <= limit { + self.buckets[i] += 1; + } + } + self.count += 1; + self.sum_secs += gap_secs; + self.sum_squares_secs += gap_secs * gap_secs; + if gap_secs > self.max_secs { self.max_secs = gap_secs; } + } + + pub fn mean_secs(&self) -> f64 { + if self.count == 0 { 0.0 } else { self.sum_secs / self.count as f64 } + } + + /// Desviación estándar muestral. Computada vía `sum_squares - n*mean²` + /// para precisión razonable sin almacenar las muestras. + pub fn stddev_secs(&self) -> f64 { + if self.count < 2 { return 0.0; } + let n = self.count as f64; + let mean = self.mean_secs(); + let var = (self.sum_squares_secs - n * mean * mean) / (n - 1.0); + // Numerical floor: var puede ser ligeramente negativo por float ε. + if var <= 0.0 { 0.0 } else { var.sqrt() } + } + + pub fn stats(&self) -> GapStats { + GapStats { + count: self.count, + mean_secs: self.mean_secs(), + stddev_secs: self.stddev_secs(), + max_secs: self.max_secs, + } + } + + pub fn bucket_limits() -> &'static [f64; 7] { &GAP_BUCKET_LIMITS_SECS } +} + +pub struct Observer { + window: VecDeque, + window_size: usize, + marginal: HashMap, + cooccur: HashMap<(EventKind, EventKind), u64>, + total: u64, + /// Last-seen timestamps para aplicar decay en query time. None = sin + /// time-decay (modo tradicional). + last_seen_marginal: HashMap, + last_seen_cooccur: HashMap<(EventKind, EventKind), Instant>, + /// Half-life del decay exponencial en segundos. None = sin decay + /// (las consultas devuelven los counts crudos). + half_life_secs: Option, + /// Histograma de gaps temporales por par (a, b). Capturado al `record()`. + gap_histograms: HashMap<(EventKind, EventKind), GapHistogram>, + /// Sets de "qué cambió desde el último snapshot". Se vacían en + /// `snapshot()` y `snapshot_delta()`. Usado para escritura incremental. + dirty_marginal: std::collections::HashSet, + dirty_cooccur: std::collections::HashSet<(EventKind, EventKind)>, +} + +impl Observer { + pub fn new(window_size: usize) -> Self { + Self { + window: VecDeque::with_capacity(window_size), + window_size, + marginal: HashMap::new(), + cooccur: HashMap::new(), + total: 0, + last_seen_marginal: HashMap::new(), + last_seen_cooccur: HashMap::new(), + half_life_secs: None, + gap_histograms: HashMap::new(), + dirty_marginal: std::collections::HashSet::new(), + dirty_cooccur: std::collections::HashSet::new(), + } + } + + /// Activa decay exponencial con half-life en segundos. λ = ln(2)/half_life. + /// Aplicado en query time sobre los counts crudos usando last_seen. + pub fn with_half_life(mut self, half_life_secs: f64) -> Self { + if half_life_secs > 0.0 { + self.half_life_secs = Some(half_life_secs); + } + self + } + + pub fn half_life(&self) -> Option { self.half_life_secs } + + /// Registra un evento. Actualiza marginales y co-ocurrencias contra todo + /// evento aún en la ventana. + pub fn record(&mut self, kind: EventKind) { + let now = Instant::now(); + let timed = TimedEvent { kind: kind.clone(), at: now }; + + // Co-ocurrencias: este evento con cada uno previo en ventana. + // Capturamos también el gap temporal (now - w.at) para histograma. + for w in &self.window { + let key = (w.kind.clone(), kind.clone()); + *self.cooccur.entry(key.clone()).or_insert(0) += 1; + self.last_seen_cooccur.insert(key.clone(), now); + let gap_secs = now.duration_since(w.at).as_secs_f64(); + self.gap_histograms.entry(key.clone()).or_default().observe(gap_secs); + self.dirty_cooccur.insert(key); + } + + self.window.push_back(timed); + if self.window.len() > self.window_size { + self.window.pop_front(); + } + + *self.marginal.entry(kind.clone()).or_insert(0) += 1; + self.last_seen_marginal.insert(kind.clone(), now); + self.dirty_marginal.insert(kind); + self.total += 1; + } + + /// Aplica el decay sobre un count crudo dado el `last_seen` correspondiente. + /// Si half_life es None, devuelve el count tal cual (sin decay). + fn decay(&self, count: u64, last_seen: Option) -> f64 { + let raw = count as f64; + let (hl, last) = match (self.half_life_secs, last_seen) { + (Some(hl), Some(t)) => (hl, t), + _ => return raw, + }; + let age_secs = Instant::now().duration_since(last).as_secs_f64(); + raw * 0.5_f64.powf(age_secs / hl) + } + + /// Marginal con decay aplicado. + pub fn marginal_decayed(&self, k: &EventKind) -> f64 { + let raw = self.marginal.get(k).copied().unwrap_or(0); + let last = self.last_seen_marginal.get(k).copied(); + self.decay(raw, last) + } + + /// Cooccurrence con decay aplicado. + pub fn cooccur_decayed(&self, a: &EventKind, b: &EventKind) -> f64 { + let raw = self.cooccur.get(&(a.clone(), b.clone())).copied().unwrap_or(0); + let last = self.last_seen_cooccur.get(&(a.clone(), b.clone())).copied(); + self.decay(raw, last) + } + + /// Entropía de Shannon de la distribución marginal de eventos. + /// H(X) = −Σ p(x) log₂ p(x). Unidad: bits. + pub fn shannon_entropy(&self) -> f64 { + if self.total == 0 { return 0.0; } + let total = self.total as f64; + self.marginal.values() + .map(|&c| { + let p = c as f64 / total; + if p > 0.0 { -p * p.log2() } else { 0.0 } + }) + .sum() + } + + /// P(b | a) = "dado que algo siguió a `a` dentro del window, qué fracción + /// fue `b`". Suma 1 sobre todos los b posibles para un a fijo. + /// + /// Implementación: cooccur_decayed(a, b) / Σ_x cooccur_decayed(a, x). + /// Si half_life is None, los decayed values son los counts crudos. + pub fn conditional_prob(&self, a: &EventKind, b: &EventKind) -> f64 { + let joint = self.cooccur_decayed(a, b); + let row_total: f64 = self.cooccur.keys() + .filter(|(x, _)| x == a) + .map(|(x, y)| self.cooccur_decayed(x, y)) + .sum(); + if row_total <= 0.0 { 0.0 } else { joint / row_total } + } + + /// Información mutua puntual entre `a` y `b` con decay aplicado: + /// PMI(a, b) = log₂( P(a, b) / (P(a) · P(b)) ). + /// Positivo → más correlacionados de lo que sugiere independencia. + pub fn pmi(&self, a: &EventKind, b: &EventKind) -> f64 { + // Total decayed: suma de marginales con decay (no usamos self.total + // directo porque debería ser consistente con los decayed values). + let total_decayed: f64 = self.marginal.keys() + .map(|k| self.marginal_decayed(k)) + .sum(); + if total_decayed <= 0.0 { return 0.0; } + let joint = self.cooccur_decayed(a, b) / total_decayed; + let pa = self.marginal_decayed(a) / total_decayed; + let pb = self.marginal_decayed(b) / total_decayed; + if joint <= 0.0 || pa <= 0.0 || pb <= 0.0 { return 0.0; } + (joint / (pa * pb)).log2() + } + + /// Información mutua acumulada de la pareja (a, b) ponderada por su + /// probabilidad conjunta. Útil como medida de "interés" del par. + pub fn weighted_pmi(&self, a: &EventKind, b: &EventKind) -> f64 { + if self.total == 0 { return 0.0; } + let joint = self.cooccur + .get(&(a.clone(), b.clone())) + .copied() + .unwrap_or(0) as f64 / self.total as f64; + joint * self.pmi(a, b) + } + + pub fn marginals(&self) -> &HashMap { &self.marginal } + + /// Última vez que se vio un kind. None si nunca o si fue restaurado + /// desde snapshot (los Instants no portables se descartan). + pub fn last_seen_marginal(&self, kind: &EventKind) -> Option { + self.last_seen_marginal.get(kind).copied() + } + pub fn cooccurrences(&self) -> &HashMap<(EventKind, EventKind), u64> { &self.cooccur } + pub fn total(&self) -> u64 { self.total } + pub fn window_size(&self) -> usize { self.window_size } + pub fn current_window(&self) -> usize { self.window.len() } + + /// Últimos N eventos del window, en orden cronológico (más viejo primero). + /// Si N > window.len(), devuelve todo el window. + pub fn recent(&self, n: usize) -> impl Iterator { + let start = self.window.len().saturating_sub(n); + self.window.range(start..) + } + + pub fn gap_histograms(&self) -> &HashMap<(EventKind, EventKind), GapHistogram> { + &self.gap_histograms + } + + /// Top-K pares por count del histograma (más frecuentes primero). + /// Útil para limitar cardinalidad de métricas exportadas. + pub fn top_gap_pairs(&self, k: usize) -> Vec<(&(EventKind, EventKind), &GapHistogram)> { + let mut pairs: Vec<_> = self.gap_histograms.iter().collect(); + pairs.sort_by(|a, b| b.1.count.cmp(&a.1.count)); + pairs.truncate(k); + pairs + } + + /// Snapshot full: estado estadístico completo. Limpia los sets dirty + /// como side-effect — los próximos `snapshot_delta()` cubren sólo los + /// cambios posteriores. + pub fn snapshot(&mut self) -> ObserverSnapshot { + self.dirty_marginal.clear(); + self.dirty_cooccur.clear(); + ObserverSnapshot { + schema_version: OBSERVER_SCHEMA_VERSION, + is_delta: false, + window_size: self.window_size, + half_life_secs: self.half_life_secs, + total: self.total, + marginal: self.marginal.iter() + .map(|(k, v)| (k.clone(), *v)) + .collect(), + cooccur: self.cooccur.iter() + .map(|((a, b), c)| (a.clone(), b.clone(), *c)) + .collect(), + gap_histograms: self.gap_histograms.iter() + .map(|((a, b), h)| (a.clone(), b.clone(), h.clone())) + .collect(), + } + } + + /// Snapshot incremental: sólo incluye los kinds y pares que cambiaron + /// desde el último `snapshot()` o `snapshot_delta()`. Útil para + /// checkpoints frecuentes con poco overhead. Limpia los sets dirty. + pub fn snapshot_delta(&mut self) -> ObserverSnapshot { + let marginal: Vec<_> = self.dirty_marginal.iter() + .filter_map(|k| self.marginal.get(k).map(|v| (k.clone(), *v))) + .collect(); + let cooccur: Vec<_> = self.dirty_cooccur.iter() + .filter_map(|(a, b)| { + self.cooccur.get(&(a.clone(), b.clone())) + .map(|c| (a.clone(), b.clone(), *c)) + }) + .collect(); + // Para histogramas: incluimos los pares cuyo cooccur cambió. + let gap_histograms: Vec<_> = self.dirty_cooccur.iter() + .filter_map(|(a, b)| { + self.gap_histograms.get(&(a.clone(), b.clone())) + .map(|h| (a.clone(), b.clone(), h.clone())) + }) + .collect(); + self.dirty_marginal.clear(); + self.dirty_cooccur.clear(); + ObserverSnapshot { + schema_version: OBSERVER_SCHEMA_VERSION, + is_delta: true, + window_size: self.window_size, + half_life_secs: self.half_life_secs, + total: self.total, + marginal, cooccur, gap_histograms, + } + } + + /// Aplica un delta sobre el estado actual. Para `is_delta=true`, los + /// valores en marginal/cooccur sobrescriben las entradas existentes. + /// Si `is_delta=false`, equivale a `from_snapshot` pero in-place. + pub fn apply_delta(&mut self, delta: ObserverSnapshot) { + let now = Instant::now(); + if !delta.is_delta { + // Full: reset state. + *self = Self::from_snapshot(delta); + return; + } + // Incremental merge. + for (k, v) in delta.marginal { + self.last_seen_marginal.insert(k.clone(), now); + self.marginal.insert(k, v); + } + for (a, b, c) in delta.cooccur { + self.last_seen_cooccur.insert((a.clone(), b.clone()), now); + self.cooccur.insert((a, b), c); + } + for (a, b, h) in delta.gap_histograms { + self.gap_histograms.insert((a, b), h); + } + // total: sólo subimos (el delta podría estar atrasado). + if delta.total > self.total { self.total = delta.total; } + } + + /// Reconstruye Observer desde un snapshot. El window queda vacío; + /// last_seen_* se inicializa en `now()` para que el decay arranque + /// "ahora" para todos los counts (aproximación razonable post-reboot). + pub fn from_snapshot(snap: ObserverSnapshot) -> Self { + let now = Instant::now(); + let mut marginal = HashMap::new(); + let mut last_seen_marginal = HashMap::new(); + for (k, v) in snap.marginal { + last_seen_marginal.insert(k.clone(), now); + marginal.insert(k, v); + } + let mut cooccur = HashMap::new(); + let mut last_seen_cooccur = HashMap::new(); + for (a, b, c) in snap.cooccur { + last_seen_cooccur.insert((a.clone(), b.clone()), now); + cooccur.insert((a, b), c); + } + let gap_histograms = snap.gap_histograms.into_iter() + .map(|(a, b, h)| ((a, b), h)) + .collect(); + Self { + window: VecDeque::with_capacity(snap.window_size), + window_size: snap.window_size, + marginal, + cooccur, + total: snap.total, + last_seen_marginal, + last_seen_cooccur, + half_life_secs: snap.half_life_secs, + gap_histograms, + dirty_marginal: std::collections::HashSet::new(), + dirty_cooccur: std::collections::HashSet::new(), + } + } +} + +const OBSERVER_SCHEMA_VERSION: u16 = 1; + +/// Snapshot serializable. Se persiste a JSON en disco y se restaura al +/// reboot para preservar contadores, co-ocurrencias e histogramas. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ObserverSnapshot { + pub schema_version: u16, + /// `true` si sólo contiene los cambios desde el último snapshot. + /// `false` = full state, sobreescribe el observer al aplicar. + #[serde(default)] + pub is_delta: bool, + pub window_size: usize, + pub half_life_secs: Option, + pub total: u64, + /// Marginales serializados como Vec porque HashMap usa + /// EventKind como key — y EventKind tiene variantes con payloads que + /// no son JSON-key-serializable (BusInvokeOf, Custom). + pub marginal: Vec<(EventKind, u64)>, + pub cooccur: Vec<(EventKind, EventKind, u64)>, + pub gap_histograms: Vec<(EventKind, EventKind, GapHistogram)>, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rules::EventKind::*; + + #[test] + fn entropy_zero_for_single_event() { + let mut obs = Observer::new(10); + for _ in 0..5 { obs.record(EnteSpawned); } + // Distribución degenerada: una sola observación posible → H = 0. + assert!(obs.shannon_entropy() < 1e-9); + } + + #[test] + fn entropy_one_for_balanced_binary() { + let mut obs = Observer::new(100); + for _ in 0..10 { obs.record(EnteSpawned); } + for _ in 0..10 { obs.record(EnteDied); } + // Bernoulli(0.5) → H = 1 bit. + assert!((obs.shannon_entropy() - 1.0).abs() < 1e-9); + } + + #[test] + fn conditional_prob_perfect_dependency() { + let mut obs = Observer::new(100); + // Spawned siempre seguido por Died. + for _ in 0..5 { + obs.record(EnteSpawned); + obs.record(EnteDied); + } + let p = obs.conditional_prob(&EnteSpawned, &EnteDied); + assert!(p > 0.0, "esperamos correlación positiva, got {p}"); + } +} diff --git a/crates/core/ente-brain/src/rules.rs b/crates/core/ente-brain/src/rules.rs new file mode 100644 index 0000000..8c17a27 --- /dev/null +++ b/crates/core/ente-brain/src/rules.rs @@ -0,0 +1,206 @@ +//! Tipos de regla. La fuente de verdad del shape es esta definición Rust; +//! `schema/rule.k` queda como referencia de diseño no cargada. +//! +//! Cargables desde JSON (array, objeto-con-array, o JSONL). El motor no +//! acepta Rules construidas a mano sin pasar por validate() — ver +//! `engine::insert`. + +use ente_card::Capability; +use serde::{Deserialize, Serialize}; +use ulid::Ulid; + +/// Triplet [Sujeto + Evento + Acción]. Inmutable tras carga. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Rule { + pub id: Ulid, + #[serde(default = "default_priority")] + pub priority: u8, + pub when: EventPattern, + pub then: Vec, + #[serde(default)] + pub scope: Scope, +} + +fn default_priority() -> u8 { 5 } + +impl Rule { + pub fn validate(&self) -> Result<(), RuleError> { + if self.then.is_empty() { + return Err(RuleError::EmptyActions); + } + self.when.validate_recursive() + } +} + +#[derive(Debug)] +pub enum RuleError { + EmptyActions, + EmptySequence, + EmptyCompound, + InvalidPriority, +} + +impl std::fmt::Display for RuleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EmptyActions => write!(f, "regla sin acciones"), + Self::EmptySequence => write!(f, "Sequence pattern con kinds vacío"), + Self::EmptyCompound => write!(f, "Either/All con patterns vacío"), + Self::InvalidPriority => write!(f, "prioridad fuera de rango"), + } + } +} + +impl std::error::Error for RuleError {} + +/// Match del sujeto. Vacío en todos los campos = match cualquier Ente. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Scope { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subject_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subject_label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subject_has_cap: Option, +} + +impl Scope { + pub fn is_wildcard(&self) -> bool { + self.subject_id.is_none() + && self.subject_label.is_none() + && self.subject_has_cap.is_none() + } +} + +/// Patrón de evento que dispara una regla. Tagged union — JSON con campo +/// `type`. Soporta composición recursiva (Either/All) sobre Single y +/// Sequence atómicos. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(tag = "type")] +pub enum EventPattern { + /// Match un único evento por kind. + Single { kind: EventKind }, + /// Match si los últimos N eventos del history coinciden en orden con + /// `kinds`, todos dentro de `within_ms` (0 = sin límite temporal). + Sequence { + kinds: Vec, + #[serde(default)] + within_ms: u64, + }, + /// OR: match si cualquier sub-pattern matchea. + Either { patterns: Vec }, + /// AND: match si todos los sub-patterns matchean simultáneamente + /// contra el mismo (event, history). + All { patterns: Vec }, +} + +impl EventPattern { + /// `true` si el pattern es atómico (no recursivo) y puede ser indexado + /// por discriminante de `EventKind` para dispatch O(1). Compuestos + /// (Either/All) se evalúan en un bucket de fallback. + pub fn is_simple(&self) -> bool { + matches!(self, Self::Single { .. } | Self::Sequence { .. }) + } + + /// Última `EventKind` que dispara la evaluación de un pattern atómico. + /// Devuelve None para compuestos. + pub fn trigger_kind(&self) -> Option<&EventKind> { + match self { + Self::Single { kind } => Some(kind), + Self::Sequence { kinds, .. } => kinds.last(), + Self::Either { .. } | Self::All { .. } => None, + } + } + + /// Validación recursiva. Compuestos vacíos se rechazan al cargar. + pub fn validate_recursive(&self) -> Result<(), RuleError> { + match self { + Self::Single { .. } => Ok(()), + Self::Sequence { kinds, .. } => { + if kinds.is_empty() { Err(RuleError::EmptySequence) } else { Ok(()) } + } + Self::Either { patterns } | Self::All { patterns } => { + if patterns.is_empty() { + return Err(RuleError::EmptyCompound); + } + for p in patterns { p.validate_recursive()?; } + Ok(()) + } + } + } +} + +/// Tipo de evento que dispara reglas. Indexado por discriminante en el motor. +/// Diseñado para que `EventKindDiscriminant::from(&kind)` sea barato (no hash +/// del payload, sólo del tag). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum EventKind { + EnteSpawned, + EnteDied, + BusAnnounce, + BusInvoke, + BusInvokeOf(Capability), + DeviceAdded, + DeviceRemoved, + Custom(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { Trace, Debug, Info, Warn, Error } + +impl LogLevel { + pub fn as_str(&self) -> &'static str { + match self { + Self::Trace => "trace", + Self::Debug => "debug", + Self::Info => "info", + Self::Warn => "warn", + Self::Error => "error", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "PascalCase")] +pub enum Action { + Log { + #[serde(default = "default_log_level")] + level: LogLevel, + message: String, + }, + Notify { + target_id: Ulid, + message: String, + }, + /// `card_blob` es JSON del EntityCard codificado en base64. El motor lo + /// decodifica y forwarda como SpawnRequest al graph. + Spawn { + card_blob: String, + }, + Invoke { + target_cap: Capability, + /// blob crudo (en JSON viaja como base64 vía `blob_b64`). + #[serde(with = "blob_b64")] + blob: Vec, + }, + Inhibit { + reason: String, + }, +} + +fn default_log_level() -> LogLevel { LogLevel::Info } + +mod blob_b64 { + use base64::{engine::general_purpose::STANDARD, Engine}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], s: S) -> Result { + s.serialize_str(&STANDARD.encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let s = String::deserialize(d)?; + STANDARD.decode(&s).map_err(serde::de::Error::custom) + } +} diff --git a/crates/core/ente-bus/Cargo.toml b/crates/core/ente-bus/Cargo.toml new file mode 100644 index 0000000..fdc1fc7 --- /dev/null +++ b/crates/core/ente-bus/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ente-bus" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +ente-card = { path = "../ente-card" } +serde = { workspace = true } +postcard = { workspace = true } +ulid = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +ente-echo = { path = "../ente-echo" } + +[[example]] +name = "busctl" +path = "examples/busctl.rs" diff --git a/crates/core/ente-bus/examples/busctl.rs b/crates/core/ente-bus/examples/busctl.rs new file mode 100644 index 0000000..0d5b36e --- /dev/null +++ b/crates/core/ente-bus/examples/busctl.rs @@ -0,0 +1,52 @@ +//! busctl: cliente CLI para el bus interno del fractal. +//! +//! Uso: +//! cargo run --example busctl -- list-entes +//! cargo run --example busctl -- announce +//! cargo run --example busctl -- power-off +//! +//! Si `ENTE_BUS_SOCK` no está en el entorno, cae al path dev por defecto. + +use ente_bus::{BusClient, BusRequest}; +use std::env; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let args: Vec = env::args().collect(); + let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("list-entes"); + + let mut client = match BusClient::from_env().await { + Ok(c) => c, + Err(_) => { + let user = env::var("USER").unwrap_or_else(|_| "ente".into()); + let runtime = env::var("XDG_RUNTIME_DIR") + .unwrap_or_else(|_| env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into())); + let path = format!("{runtime}/ente-bus-{user}.sock"); + eprintln!("ENTE_BUS_SOCK no definido, intentando {path}"); + BusClient::connect(&path).await? + } + }; + + let req = match cmd { + "list-entes" => BusRequest::ListEntes, + "announce" => BusRequest::Announce { capabilities: vec![] }, + "power-off" => BusRequest::PowerOff { interactive: false }, + "reboot" => BusRequest::Reboot { interactive: false }, + "suspend" => BusRequest::Suspend { interactive: false }, + "invoke-echo" => { + let msg = args.get(2).map(|s| s.as_str()).unwrap_or("hola"); + BusRequest::Invoke { + cap: ente_echo::echo_capability(), + blob: msg.as_bytes().to_vec(), + } + } + other => { + eprintln!("subcomando desconocido: {other}"); + eprintln!("válidos: list-entes, announce, power-off, reboot, suspend, invoke-echo "); + std::process::exit(2); + } + }; + let resp = client.call(req).await?; + println!("{resp:#?}"); + Ok(()) +} diff --git a/crates/core/ente-bus/src/lib.rs b/crates/core/ente-bus/src/lib.rs new file mode 100644 index 0000000..586df9c --- /dev/null +++ b/crates/core/ente-bus/src/lib.rs @@ -0,0 +1,228 @@ +//! ente-bus: bus de capacidades interno del fractal. +//! +//! Wire format: Unix SOCK_STREAM con framing length-prefijo (u32 BE) + payload +//! postcard. Bidireccional pero por ahora request-response síncrono. +//! +//! Identidad: cada Ente hijo recibe `ENTE_BUS_SOCK` y `ENTE_ID` en su entorno. +//! El cliente lee ambos vía `BusClient::from_env`. + +use ente_card::Capability; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::str::FromStr; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::UnixStream; +use ulid::Ulid; + +pub const ENV_BUS_SOCK: &str = "ENTE_BUS_SOCK"; +pub const ENV_ENTE_ID: &str = "ENTE_ID"; +pub const MAX_FRAME: usize = 1 << 20; // 1 MiB — protección contra OOM + +/// Interface UUID para decisiones de policy. Un Ente independiente +/// (separado de polkit-compat) se anuncia como proveedor de +/// `Capability::Endpoint { interface: POLKIT_DECISION_IFACE, version: 1 }` +/// para arbitrar autorizaciones. Recibe blob: +/// `pid_be | uid_be | action_id_utf8` → responde 1 byte: 1=allow, 0=deny. +pub const POLKIT_DECISION_IFACE: ente_card::InterfaceId = + ente_card::InterfaceId([0xb0; 16]); + +/// Interface UUID auto-anunciado por compat-polkit. Diferente al de +/// decisión para evitar recursión (polkit-compat invoca DECISION pero +/// no es proveedor de DECISION; se anuncia como SERVICE). +pub const POLKIT_SERVICE_IFACE: ente_card::InterfaceId = + ente_card::InterfaceId([0xa4; 16]); + +/// Credenciales del peer extraídas vía SO_PEERCRED al accept del bus. +/// Imposibles de falsear desde el cliente — el kernel las inyecta. +/// Definidas aquí (no en ente-zero) porque conceptualmente son atributo +/// del protocolo del bus, no del init. +#[derive(Debug, Clone, Copy)] +pub struct PeerCreds { + pub pid: i32, + pub uid: u32, + pub gid: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BusMessage { + pub from: Option, + pub seq: u64, + pub payload: BusPayload, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BusPayload { + Request(BusRequest), + Response(BusResponse), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BusRequest { + /// Saludo. El Ente anuncia que está vivo y declara sus capacidades. + /// El Init usa esto para saber que el child arrancó correctamente, + /// independientemente de su exit code. + Announce { capabilities: Vec }, + + /// Listar Entes vivos. Útil para debugging y para Entes-supervisor. + ListEntes, + + /// Control de estado del fractal. Traducido desde D-Bus por compat-logind. + PowerOff { interactive: bool }, + Reboot { interactive: bool }, + Suspend { interactive: bool }, + Hibernate { interactive: bool }, + + /// Invocación genérica de capacidad. `cap` debe estar provista por algún + /// Ente del grafo; el blob es el argumento opaco que el proveedor parsea. + Invoke { cap: Capability, blob: Vec }, + + /// Actualización dinámica del set de capacidades del Ente que llama. + /// Sólo aplicable al `from_authenticated` — un Ente sólo puede modificar + /// sus propias caps. La Card original (immutable) no se toca; la mutación + /// va al `dynamic_provides` del Incarnated. + UpdateCapabilities { + adds: Vec, + removes: Vec, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BusResponse { + Ok, + Error(String), + Entes(Vec), + Invoked { result: Vec }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnteInfo { + pub id: Ulid, + pub label: String, + pub provides: Vec, + pub pid: Option, +} + +pub async fn write_frame(w: &mut W, msg: &BusMessage) -> anyhow::Result<()> { + let bytes = postcard::to_stdvec(msg)?; + if bytes.len() > MAX_FRAME { + anyhow::bail!("frame too large: {} > {}", bytes.len(), MAX_FRAME); + } + w.write_u32(bytes.len() as u32).await?; + w.write_all(&bytes).await?; + Ok(()) +} + +pub async fn read_frame(r: &mut R) -> anyhow::Result { + let len = r.read_u32().await? as usize; + if len > MAX_FRAME { + anyhow::bail!("frame oversize: {len}"); + } + let mut buf = vec![0u8; len]; + r.read_exact(&mut buf).await?; + Ok(postcard::from_bytes(&buf)?) +} + +pub struct BusClient { + stream: UnixStream, + seq: u64, + self_id: Option, +} + +/// Trait que un Ente proveedor implementa para servir invokes que el bus le +/// forwarda. Sync por simplicidad — un handler async se cubre con +/// `tokio::task::block_in_place` o un canal hacia un task externo. +pub trait InvokeHandler { + fn handle(&mut self, cap: ente_card::Capability, blob: Vec) -> BusResponse; +} + +/// Conexión long-lived para Entes que proveen capacidades. A diferencia de +/// `BusClient` (request-response y desconecta), `BusServer`: +/// 1. Anuncia su identidad al bus +/// 2. Mantiene la conexión abierta +/// 3. Atiende invokes forwardeados por el bus en bucle +pub struct BusServer { + stream: UnixStream, + self_id: Ulid, +} + +impl BusServer { + pub async fn from_env() -> anyhow::Result { + let path = std::env::var(ENV_BUS_SOCK) + .map_err(|_| anyhow::anyhow!("{} no definido", ENV_BUS_SOCK))?; + let id_s = std::env::var(ENV_ENTE_ID) + .map_err(|_| anyhow::anyhow!("{} no definido", ENV_ENTE_ID))?; + let self_id = Ulid::from_str(&id_s) + .map_err(|_| anyhow::anyhow!("{} no es un Ulid válido: {id_s}", ENV_ENTE_ID))?; + let stream = UnixStream::connect(&path).await?; + Ok(Self { stream, self_id }) + } + + pub async fn announce(&mut self, capabilities: Vec) -> anyhow::Result<()> { + let req = BusMessage { + from: Some(self.self_id), + seq: 1, + payload: BusPayload::Request(BusRequest::Announce { capabilities }), + }; + write_frame(&mut self.stream, &req).await?; + let resp = read_frame(&mut self.stream).await?; + match resp.payload { + BusPayload::Response(BusResponse::Ok) => Ok(()), + BusPayload::Response(other) => anyhow::bail!("Announce rechazado: {other:?}"), + BusPayload::Request(_) => anyhow::bail!("expected Response, got Request"), + } + } + + /// Bucle principal del proveedor. Atiende invokes hasta que la conexión + /// se cierra (el bus muere o el Ente recibe SIGTERM y nosotros dropeamos). + pub async fn serve(mut self, mut handler: H) -> anyhow::Result<()> { + loop { + let msg = read_frame(&mut self.stream).await?; + let resp = match msg.payload { + BusPayload::Request(BusRequest::Invoke { cap, blob }) => { + handler.handle(cap, blob) + } + BusPayload::Request(other) => { + BusResponse::Error(format!("BusServer no maneja {other:?}")) + } + BusPayload::Response(_) => continue, + }; + let out = BusMessage { + from: Some(self.self_id), + seq: msg.seq, + payload: BusPayload::Response(resp), + }; + write_frame(&mut self.stream, &out).await?; + } + } +} + +impl BusClient { + pub async fn connect(path: impl AsRef) -> anyhow::Result { + let stream = UnixStream::connect(path).await?; + let self_id = std::env::var(ENV_ENTE_ID) + .ok() + .and_then(|s| Ulid::from_str(&s).ok()); + Ok(Self { stream, seq: 0, self_id }) + } + + pub async fn from_env() -> anyhow::Result { + let path = std::env::var(ENV_BUS_SOCK) + .map_err(|_| anyhow::anyhow!("{} no definido", ENV_BUS_SOCK))?; + Self::connect(&path).await + } + + pub async fn call(&mut self, req: BusRequest) -> anyhow::Result { + self.seq = self.seq.wrapping_add(1); + let msg = BusMessage { + from: self.self_id, + seq: self.seq, + payload: BusPayload::Request(req), + }; + write_frame(&mut self.stream, &msg).await?; + let resp = read_frame(&mut self.stream).await?; + match resp.payload { + BusPayload::Response(r) => Ok(r), + BusPayload::Request(_) => anyhow::bail!("expected response, got request"), + } + } +} diff --git a/crates/core/ente-card/Cargo.toml b/crates/core/ente-card/Cargo.toml new file mode 100644 index 0000000..f6352b3 --- /dev/null +++ b/crates/core/ente-card/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ente-card" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +ulid = { workspace = true } diff --git a/crates/core/ente-card/schema/card.k b/crates/core/ente-card/schema/card.k new file mode 100644 index 0000000..1c1232b --- /dev/null +++ b/crates/core/ente-card/schema/card.k @@ -0,0 +1,178 @@ +# ============================================================================ +# card.k — REFERENCE ONLY. NOT LOADED. +# +# La validación canónica de EntityCard vive en Rust: +# crates/ente-card/src/lib.rs :: EntityCard::validate() +# El loader (crates/ente-brain/src/loader.rs) sólo acepta JSON. +# +# Este archivo se conserva como notas de diseño legibles para humanos sobre +# las invariantes que `validate()` debe garantizar. Si modificas el shape +# en Rust, sincroniza este archivo a mano (o reemplázalo por JSON Schema +# generado vía `schemars`). +# ============================================================================ + +# ---------- Identidad ---------- + +schema EntityCard: + """Tarjeta de Identidad. Inmutable: cambios = nueva Card con nuevo id.""" + schema_version: int = 1 + id: str # Ulid (26 chars, Crockford base32) + lineage?: str # parent Ulid; None = Ente raíz + label: str # legible, no es identificador + provides: [Capability] = [] # contrato hacia el grafo + requires: [Capability] = [] # contrato del grafo hacia el Ente + soma: SomaSpec # cuerpo: aislamiento + recursos + payload: Payload # cómo encarnar (Wasm/Native/Virtual) + supervision: Supervision # política tras muerte + genesis?: [EntityCard] = [] # hijos a instanciar al encarnar + + check: + schema_version == 1, "schema version no soportada" + len(label) > 0, "label vacío" + len(id) == 26, "id debe ser Ulid (26 caracteres)" + # Auto-dependencia: una capacidad no puede estar en requires y provides + all c in requires { c not in provides }, "self-dependency: ${c}" + + +# ---------- Capacidades (typed enum) ---------- + +# KCL no tiene sum types nativos; usamos tagged union: `kind` + campos opcionales +# que sólo aplican según el kind. Las invariantes en `check:` aseguran consistencia. +schema Capability: + """Capacidad tipada del fractal. NUNCA usar strings libres.""" + kind: "FilesystemRoot" | "KernelNetlink" | "Endpoint" | "LegacyLogind" | "Device" | "Spawn" | "Journal" + netlink_family?: "Uevent" | "Route" | "Generic" | "Audit" + endpoint_interface?: str # 32-char hex (UUID 16 bytes) + endpoint_version?: int + device_class?: "Block" | "Tty" | "Input" | "Drm" | "Net" | "Hidraw" + + check: + kind != "KernelNetlink" or netlink_family is not None, \ + "KernelNetlink requiere netlink_family" + kind != "Endpoint" or (endpoint_interface is not None and endpoint_version is not None), \ + "Endpoint requiere interface + version" + kind != "Endpoint" or len(endpoint_interface) == 32, \ + "endpoint_interface debe ser hex de 32 chars" + kind != "Device" or device_class is not None, \ + "Device requiere device_class" + + +# ---------- Soma: cuerpo + restricciones de recursos ---------- + +schema SomaSpec: + """Aislamiento + recursos. Validados por KCL antes de tocar el kernel.""" + namespaces: NamespaceSet = NamespaceSet {} + rlimits: ResourceLimits = ResourceLimits {} + cgroup: CgroupSpec = CgroupSpec {} + cpu_affinity?: [int] # CPU pinning + + check: + cpu_affinity is None or all c in cpu_affinity { c >= 0 and c < 1024 }, \ + "cpu_affinity fuera de rango [0, 1024)" + + +schema NamespaceSet: + mount: bool = False + pid: bool = False + net: bool = False + uts: bool = False + ipc: bool = False + user: bool = False + cgroup: bool = False + + +schema ResourceLimits: + """Restricciones nativas validadas en KCL — el kernel sólo ve valores sanos.""" + mem_bytes?: int # RLIMIT_AS + nproc?: int # RLIMIT_NPROC + nofile?: int # RLIMIT_NOFILE + energy_budget_mw?: int # presupuesto energético (futuro) + + check: + mem_bytes is None or mem_bytes > 0, "mem_bytes debe ser positivo" + mem_bytes is None or mem_bytes <= 1099511627776, "mem_bytes > 1 TiB sospechoso" + nproc is None or (nproc > 0 and nproc <= 65535), "nproc fuera de rango" + nofile is None or (nofile > 0 and nofile <= 1048576), "nofile fuera de rango" + energy_budget_mw is None or energy_budget_mw > 0, "energy_budget_mw debe ser positivo" + + +schema CgroupSpec: + """Cgroup v2: path + weights. cpu_weight 1..10000 según kernel docs.""" + path: str = "" + cpu_weight?: int + io_weight?: int + + check: + cpu_weight is None or (cpu_weight >= 1 and cpu_weight <= 10000), \ + "cpu_weight 1..10000" + io_weight is None or (io_weight >= 1 and io_weight <= 10000), \ + "io_weight 1..10000" + + +# ---------- Payload: tagged union de cómo encarnar ---------- + +schema Payload: + """Una variante por Card. Set exactly one of: Wasm, Native, Virtual, Legacy.""" + kind: "Wasm" | "Native" | "Virtual" | "Legacy" + # Wasm + module_sha256?: str # hex 64 chars + entry?: str + # Native / Legacy + exec?: str + argv?: [str] = [] + envp?: [{str: str}] = [] + # Legacy + fakes?: ["SystemdLogind" | "SystemdHostnamed" | "SystemdNotify"] = [] + + check: + kind != "Wasm" or (module_sha256 is not None and entry is not None), \ + "Wasm requiere module_sha256 + entry" + kind != "Wasm" or len(module_sha256) == 64, "module_sha256 debe ser hex de 64 chars" + kind != "Native" or exec is not None, "Native requiere exec" + kind != "Legacy" or exec is not None, "Legacy requiere exec" + + +# ---------- Supervision ---------- + +schema Supervision: + kind: "Restart" | "OneShot" | "Delegate" + initial_ms?: int # ms — backoff inicial para Restart + max_ms?: int # ms — backoff máximo + + check: + kind != "Restart" or (initial_ms is not None and max_ms is not None), \ + "Restart requiere initial_ms + max_ms" + initial_ms is None or initial_ms >= 0, "initial_ms negativo" + max_ms is None or max_ms >= initial_ms or max_ms is None, \ + "max_ms < initial_ms es contradictorio" + + +# ============================================================================ +# Herencia: EnteWeb hereda de EnteBase con campos pre-rellenados. +# ============================================================================ + +schema EnteBase(EntityCard): + """Base para Entes managed: declara Spawn provider y Journal por defecto.""" + schema_version = 1 + supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000} + soma = SomaSpec { + rlimits = ResourceLimits {nofile = 4096} + cgroup = CgroupSpec {path = "ente.slice/managed", cpu_weight = 100} + } + + +schema EnteWeb(EnteBase): + """Hereda EnteBase, declara endpoint + cap LegacyLogind como ejemplo.""" + provides = [ + Capability {kind = "Journal"} + Capability { + kind = "Endpoint" + endpoint_interface = "deadbeefcafe1234deadbeefcafe1234" + endpoint_version = 1 + } + ] + soma = SomaSpec { + namespaces = NamespaceSet {net = True, mount = True, pid = True} + rlimits = ResourceLimits {nofile = 16384, mem_bytes = 536870912} # 512 MiB + cgroup = CgroupSpec {path = "ente.slice/web", cpu_weight = 200, io_weight = 100} + } diff --git a/crates/core/ente-card/src/lib.rs b/crates/core/ente-card/src/lib.rs new file mode 100644 index 0000000..4449071 --- /dev/null +++ b/crates/core/ente-card/src/lib.rs @@ -0,0 +1,326 @@ +//! ente-card: definición de la Tarjeta de Identidad del Ente. +//! +//! Una `EntityCard` no describe un proceso — describe una identidad en el +//! grafo del fractal. El Init la lee y decide cómo *encarnarla*: como ELF +//! nativo, módulo Wasm, wrapper legacy, o nodo virtual sin proceso. + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; +use std::fmt; +use std::time::Duration; +use ulid::Ulid; + +/// Versión del esquema de la Card. Cambiar = romper compatibilidad del fractal. +pub const CARD_SCHEMA_VERSION: u16 = 1; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EntityCard { + pub schema_version: u16, + pub id: Ulid, + pub lineage: Option, + pub label: String, + pub provides: BTreeSet, + pub requires: BTreeSet, + pub soma: SomaSpec, + pub payload: Payload, + pub supervision: Supervision, + /// Hijos a instanciar inmediatamente cuando esta Card se encarna. Se + /// consumen una vez (no se replican en restarts del padre — el grafo + /// re-emerge desde la Semilla viva, no desde la persistencia de la Card). + #[serde(default)] + pub genesis: Vec, +} + +impl EntityCard { + pub fn validate(&self) -> Result<(), CardError> { + if self.schema_version != CARD_SCHEMA_VERSION { + return Err(CardError::SchemaMismatch { + got: self.schema_version, + expected: CARD_SCHEMA_VERSION, + }); + } + if self.label.is_empty() { + return Err(CardError::EmptyLabel); + } + if self.label.len() > 256 { + return Err(CardError::LabelTooLong(self.label.len())); + } + // Una capacidad simultáneamente en `requires` y `provides` indica un + // ciclo de auto-dependencia que el grafo no puede resolver. + for cap in &self.requires { + if self.provides.contains(cap) { + return Err(CardError::SelfDependency(cap.clone())); + } + } + // Coherencia del payload con sus invariantes. + validate_payload(&self.payload)?; + // ResourceLimits: rangos sanos. + validate_rlimits(&self.soma.rlimits)?; + // Cgroup weights: 1..10000 según docs del kernel cgroup v2. + validate_cgroup(&self.soma.cgroup)?; + // Validación recursiva de genesis. Si una hija es inválida, la + // Semilla entera se rechaza — falla rápida en boot. + for child in &self.genesis { + child.validate()?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum CardError { + SchemaMismatch { got: u16, expected: u16 }, + EmptyLabel, + LabelTooLong(usize), + SelfDependency(Capability), + EmptyExec, + SentinelWasmHash, + InvalidRlimit(&'static str), + InvalidCgroupWeight(&'static str), +} + +impl fmt::Display for CardError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SchemaMismatch { got, expected } => { + write!(f, "schema version mismatch: got {got}, expected {expected}") + } + Self::EmptyLabel => write!(f, "card label is empty"), + Self::LabelTooLong(n) => write!(f, "label demasiado largo ({n} bytes, max 256)"), + Self::SelfDependency(c) => write!(f, "card both requires and provides {c:?}"), + Self::EmptyExec => write!(f, "Native/Legacy payload con exec vacío"), + Self::SentinelWasmHash => write!(f, "Wasm payload con sha256 sentinel (todo ceros)"), + Self::InvalidRlimit(s) => write!(f, "rlimit inválido: {s}"), + Self::InvalidCgroupWeight(s) => write!(f, "cgroup weight fuera de rango [1,10000]: {s}"), + } + } +} + +impl std::error::Error for CardError {} + +fn validate_payload(p: &Payload) -> Result<(), CardError> { + match p { + Payload::Native { exec, .. } | Payload::Legacy { exec, .. } => { + if exec.trim().is_empty() { + return Err(CardError::EmptyExec); + } + } + Payload::Wasm { module_sha256, .. } => { + // Sentinel [0u8; 32] indica "no resoluble" — usado en dev como + // fallback. En prod debe ser un hash real. + if module_sha256.iter().all(|&b| b == 0) { + return Err(CardError::SentinelWasmHash); + } + } + Payload::Virtual => {} + } + Ok(()) +} + +fn validate_rlimits(rl: &ResourceLimits) -> Result<(), CardError> { + if let Some(m) = rl.mem_bytes { + if m == 0 { return Err(CardError::InvalidRlimit("mem_bytes=0")); } + if m > 1u64 << 40 { return Err(CardError::InvalidRlimit("mem_bytes>1TiB")); } + } + if let Some(n) = rl.nproc { + if n == 0 || n > 65535 { return Err(CardError::InvalidRlimit("nproc fuera de [1,65535]")); } + } + if let Some(n) = rl.nofile { + if n == 0 || n > 1_048_576 { return Err(CardError::InvalidRlimit("nofile fuera de [1,1M]")); } + } + Ok(()) +} + +fn validate_cgroup(cg: &CgroupSpec) -> Result<(), CardError> { + if let Some(w) = cg.cpu_weight { + if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("cpu_weight")); } + } + if let Some(w) = cg.io_weight { + if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("io_weight")); } + } + Ok(()) +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum Capability { + /// Provee un punto de montaje root para Entes hijos. + FilesystemRoot, + /// Acceso a una familia de netlink. + KernelNetlink(NetlinkFamily), + /// Endpoint del bus interno del fractal — equivalente tipado de un nombre + /// D-Bus, sin la string libre. + Endpoint { interface: InterfaceId, version: u16 }, + /// Reemplazo del shim de systemd-logind. Solo Ente #compat-logind lo provee. + LegacyLogind, + /// Acceso crudo a una clase de dispositivo. Capacidad escalada. + Device { class: DeviceClass }, + /// Permiso de instanciar Entes hijos. Por defecto solo PID 1 lo tiene. + Spawn, + /// Acceso a logging estructurado del fractal. + Journal, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum NetlinkFamily { Uevent, Route, Generic, Audit } + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum DeviceClass { Block, Tty, Input, Drm, Net, Hidraw } + +/// Identificador de interfaz del bus interno. UUID, no string. Para extender +/// el protocolo del fractal, generas un UUID nuevo y versionas. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct InterfaceId(pub [u8; 16]); + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct SomaSpec { + pub namespaces: NamespaceSet, + pub rlimits: ResourceLimits, + pub cgroup: CgroupSpec, + pub cpu_affinity: Option>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct NamespaceSet { + pub mount: bool, + pub pid: bool, + pub net: bool, + pub uts: bool, + pub ipc: bool, + pub user: bool, + pub cgroup: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ResourceLimits { + pub mem_bytes: Option, + pub nproc: Option, + pub nofile: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CgroupSpec { + pub path: String, + pub cpu_weight: Option, + pub io_weight: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Payload { + Wasm { module_sha256: [u8; 32], entry: String }, + Native { + exec: String, + argv: Vec, + envp: Vec<(String, String)>, + }, + /// Sin proceso. Nodo lógico del grafo (agregadores, mediators). + Virtual, + /// Wrapper de daemon legacy. `fakes` activa shims D-Bus / sd_notify. + Legacy { + exec: String, + argv: Vec, + fakes: BTreeSet, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum LegacyFacade { + SystemdLogind, + SystemdHostnamed, + SystemdNotify, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Supervision { + Restart { + #[serde(with = "duration_millis")] + initial: Duration, + #[serde(with = "duration_millis")] + max: Duration, + }, + OneShot, + Delegate, +} + +mod duration_millis { + use serde::{Deserialize, Deserializer, Serializer}; + use std::time::Duration; + + pub fn serialize(d: &Duration, s: S) -> Result { + s.serialize_u64(d.as_millis() as u64) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let ms = u64::deserialize(d)?; + Ok(Duration::from_millis(ms)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn seed_card_validates() { + let card = EntityCard { + schema_version: CARD_SCHEMA_VERSION, + id: Ulid::new(), + lineage: None, + label: "ente-zero".into(), + provides: [Capability::Spawn, Capability::Journal].into_iter().collect(), + requires: BTreeSet::new(), + soma: SomaSpec::default(), + payload: Payload::Virtual, + supervision: Supervision::OneShot, + genesis: vec![], + }; + card.validate().unwrap(); + } + + #[test] + fn self_dependency_rejected() { + let mut s = BTreeSet::new(); + s.insert(Capability::Journal); + let card = EntityCard { + schema_version: CARD_SCHEMA_VERSION, + id: Ulid::new(), + lineage: None, + label: "bad".into(), + provides: s.clone(), + requires: s, + soma: SomaSpec::default(), + payload: Payload::Virtual, + supervision: Supervision::OneShot, + genesis: vec![], + }; + assert!(matches!(card.validate(), Err(CardError::SelfDependency(_)))); + } + + #[test] + fn invalid_genesis_propagates() { + let bad_child = EntityCard { + schema_version: CARD_SCHEMA_VERSION, + id: Ulid::new(), + lineage: None, + label: "".into(), + provides: BTreeSet::new(), + requires: BTreeSet::new(), + soma: SomaSpec::default(), + payload: Payload::Virtual, + supervision: Supervision::OneShot, + genesis: vec![], + }; + let parent = EntityCard { + schema_version: CARD_SCHEMA_VERSION, + id: Ulid::new(), + lineage: None, + label: "parent".into(), + provides: BTreeSet::new(), + requires: BTreeSet::new(), + soma: SomaSpec::default(), + payload: Payload::Virtual, + supervision: Supervision::OneShot, + genesis: vec![bad_child], + }; + assert!(matches!(parent.validate(), Err(CardError::EmptyLabel))); + } +} diff --git a/crates/core/ente-cas/Cargo.toml b/crates/core/ente-cas/Cargo.toml new file mode 100644 index 0000000..df0c34c --- /dev/null +++ b/crates/core/ente-cas/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ente-cas" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +sha2 = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/core/ente-cas/src/lib.rs b/crates/core/ente-cas/src/lib.rs new file mode 100644 index 0000000..91de37a --- /dev/null +++ b/crates/core/ente-cas/src/lib.rs @@ -0,0 +1,120 @@ +//! Content-addressable store. Resuelve `Payload::Wasm.module_sha256` (y en +//! el futuro otros payloads firmados) desde el sistema de archivos con +//! verificación de hash. Path por defecto: `$XDG_DATA_HOME/ente/cas/`. +//! +//! Override por env: `ENTE_CAS_ROOT`. + +use sha2::{Digest, Sha256}; +use std::path::PathBuf; +use tracing::debug; + +pub fn cas_root() -> PathBuf { + if let Ok(p) = std::env::var("ENTE_CAS_ROOT") { + return p.into(); + } + let base = if let Ok(d) = std::env::var("XDG_DATA_HOME") { + d + } else if let Ok(h) = std::env::var("HOME") { + format!("{h}/.local/share") + } else { + "/var/lib".into() + }; + PathBuf::from(base).join("ente").join("cas") +} + +pub fn sha256_of(bytes: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hasher.finalize().into() +} + +pub fn hex(sha: &[u8; 32]) -> String { + let mut s = String::with_capacity(64); + for b in sha { + s.push_str(&format!("{:02x}", b)); + } + s +} + +/// Lista todos los SHAs presentes en el CAS. Cada entrada del directorio +/// con nombre de 64 chars hex se considera un blob válido. +pub fn list_all_shas() -> anyhow::Result> { + let root = cas_root(); + if !root.exists() { return Ok(Vec::new()); } + let mut out = Vec::new(); + for entry in std::fs::read_dir(&root)? { + let e = entry?; + let name = e.file_name(); + let s = match name.to_str() { + Some(s) if s.len() == 64 => s, + _ => continue, + }; + let mut sha = [0u8; 32]; + let mut ok = true; + for i in 0..32 { + match u8::from_str_radix(&s[i*2..i*2+2], 16) { + Ok(b) => sha[i] = b, + Err(_) => { ok = false; break; } + } + } + if ok { out.push(sha); } + } + Ok(out) +} + +/// Garbage collector. Borra todos los blobs que no están en `reachable`. +/// Devuelve (deleted_count, freed_bytes). El caller construye `reachable` +/// caminando todas las raíces (audit chain head, Wasm SHAs en Cards, etc). +/// +/// Idempotente: re-correr no hace nada si el set no cambió. +pub fn gc(reachable: &std::collections::HashSet<[u8; 32]>) -> anyhow::Result<(usize, u64)> { + let root = cas_root(); + let mut deleted = 0usize; + let mut freed = 0u64; + for sha in list_all_shas()? { + if reachable.contains(&sha) { continue; } + let path = root.join(hex(&sha)); + if let Ok(meta) = std::fs::metadata(&path) { + freed += meta.len(); + } + if std::fs::remove_file(&path).is_ok() { + deleted += 1; + tracing::debug!(sha = %hex(&sha), "CAS gc removed"); + } + } + Ok((deleted, freed)) +} + +pub fn resolve(sha: &[u8; 32]) -> anyhow::Result> { + let path = cas_root().join(hex(sha)); + let bytes = std::fs::read(&path) + .map_err(|e| anyhow::anyhow!("CAS read {}: {e}", path.display()))?; + let actual = sha256_of(&bytes); + if &actual != sha { + anyhow::bail!( + "CAS hash mismatch en {}: declarado={} real={}", + path.display(), hex(sha), hex(&actual) + ); + } + Ok(bytes) +} + +/// Almacena bytes en el CAS, devuelve su SHA. Idempotente: si el archivo ya +/// existe con el mismo hash, no reescribe. +pub fn store(bytes: &[u8]) -> anyhow::Result<[u8; 32]> { + let sha = sha256_of(bytes); + let root = cas_root(); + std::fs::create_dir_all(&root) + .map_err(|e| anyhow::anyhow!("CAS mkdir {}: {e}", root.display()))?; + let path = root.join(hex(&sha)); + if !path.exists() { + // Escritura atómica: crear .tmp y rename. + let tmp = path.with_extension("tmp"); + std::fs::write(&tmp, bytes) + .map_err(|e| anyhow::anyhow!("CAS write {}: {e}", tmp.display()))?; + std::fs::rename(&tmp, &path) + .map_err(|e| anyhow::anyhow!("CAS rename {}: {e}", path.display()))?; + debug!(hex = %hex(&sha), len = bytes.len(), path = %path.display(), "CAS store"); + } + Ok(sha) +} diff --git a/crates/core/ente-echo/Cargo.toml b/crates/core/ente-echo/Cargo.toml new file mode 100644 index 0000000..f5d3f7d --- /dev/null +++ b/crates/core/ente-echo/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ente-echo" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[lib] +name = "ente_echo" +path = "src/lib.rs" + +[[bin]] +name = "ente-echo" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/core/ente-echo/src/lib.rs b/crates/core/ente-echo/src/lib.rs new file mode 100644 index 0000000..8ebcc32 --- /dev/null +++ b/crates/core/ente-echo/src/lib.rs @@ -0,0 +1,18 @@ +//! Constantes públicas del Ente echo. Lib aparte del bin para que `busctl` +//! y otros consumidores puedan importar el InterfaceId sin enlazar el binario. + +use ente_card::{Capability, InterfaceId}; + +/// UUID estable del interface "echo". Genera nuevo por sed si forkeas. +pub const ECHO_IFACE: InterfaceId = InterfaceId([ + 0xec, 0x40, 0xa1, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x80, 0x00, 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, +]); +pub const ECHO_VERSION: u16 = 1; + +pub fn echo_capability() -> Capability { + Capability::Endpoint { + interface: ECHO_IFACE, + version: ECHO_VERSION, + } +} diff --git a/crates/core/ente-echo/src/main.rs b/crates/core/ente-echo/src/main.rs new file mode 100644 index 0000000..8ef07ea --- /dev/null +++ b/crates/core/ente-echo/src/main.rs @@ -0,0 +1,46 @@ +//! ente-echo: Ente proveedor mínimo. Anuncia Capability::Endpoint(ECHO) y +//! responde a invokes echando el blob recibido. Vehículo para validar el +//! forwarding bus → proveedor → bus → originator. + +use ente_bus::{BusResponse, BusServer, InvokeHandler}; +use ente_card::Capability; +use ente_echo::{echo_capability, ECHO_IFACE, ECHO_VERSION}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; + +struct EchoHandler; + +impl InvokeHandler for EchoHandler { + fn handle(&mut self, cap: Capability, blob: Vec) -> BusResponse { + match cap { + Capability::Endpoint { interface, version } + if interface == ECHO_IFACE && version == ECHO_VERSION => + { + let preview = String::from_utf8_lossy(&blob).into_owned(); + info!(text = %preview, len = blob.len(), "echo invoke"); + BusResponse::Invoked { result: blob } + } + other => { + warn!(?other, "ente-echo: capacidad no soportada"); + BusResponse::Error("ente-echo solo maneja ECHO_IFACE".into()) + } + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_echo=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); + + info!("ente-echo arrancando"); + let mut server = BusServer::from_env().await?; + server.announce(vec![echo_capability()]).await?; + info!("Announce OK, sirviendo invokes"); + + if let Err(e) = server.serve(EchoHandler).await { + warn!(?e, "serve terminó"); + } + Ok(()) +} diff --git a/crates/core/ente-hostnamed-compat/Cargo.toml b/crates/core/ente-hostnamed-compat/Cargo.toml new file mode 100644 index 0000000..04e3568 --- /dev/null +++ b/crates/core/ente-hostnamed-compat/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ente-hostnamed-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-hostnamed-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +nix = { workspace = true } +libc = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/core/ente-hostnamed-compat/src/main.rs b/crates/core/ente-hostnamed-compat/src/main.rs new file mode 100644 index 0000000..ece91a7 --- /dev/null +++ b/crates/core/ente-hostnamed-compat/src/main.rs @@ -0,0 +1,340 @@ +//! ente-hostnamed-compat: shim de `org.freedesktop.hostname1`. +//! +//! GNOME control-center y otros componentes consultan este servicio al boot +//! para mostrar nombre de host, OS, kernel. Sin esto los settings panels +//! se rompen aunque el sistema funcione. +//! +//! Read-only properties: leemos /etc/hostname, /etc/os-release, uname(). +//! Set* methods: log + forward al bus interno (no aplicamos cambios reales +//! en el stub — un siguiente paso es persistir a /etc/* y rehash). + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::sync::Mutex; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface, Connection}; + +const BUS_NAME: &str = "org.freedesktop.hostname1"; +const OBJ_PATH: &str = "/org/freedesktop/hostname1"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-hostnamed-compat: arrancando"); + announce_to_fractal().await; + + let manager = HostnameManager::default(); + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { + warn!(?e, "build conn falló — modo idle"); + wait_for_term().await + } + }, + Err(e) => { + warn!(?e, "builder D-Bus falló — modo idle"); + wait_for_term().await + } + } +} + +#[derive(Default)] +struct HostnameManager { + /// Cache para SetHostname. En el stub no persistimos a /etc. + transient_hostname: Mutex>, +} + +#[interface(name = "org.freedesktop.hostname1")] +impl HostnameManager { + // ----- Properties read-only ----- + + #[zbus(property)] + async fn hostname(&self) -> String { + if let Some(h) = self.transient_hostname.lock().unwrap().clone() { + return h; + } + gethostname_libc().unwrap_or_else(|| "localhost".into()) + } + + #[zbus(property)] + async fn static_hostname(&self) -> String { + std::fs::read_to_string("/etc/hostname") + .map(|s| s.trim().to_string()) + .unwrap_or_default() + } + + #[zbus(property)] + async fn pretty_hostname(&self) -> String { + read_machine_info_field("PRETTY_HOSTNAME").unwrap_or_default() + } + + #[zbus(property)] + async fn icon_name(&self) -> String { + read_machine_info_field("ICON_NAME").unwrap_or_default() + } + + #[zbus(property)] + async fn chassis(&self) -> String { + read_machine_info_field("CHASSIS").unwrap_or_else(|| "desktop".into()) + } + + #[zbus(property)] + async fn deployment(&self) -> String { + read_machine_info_field("DEPLOYMENT").unwrap_or_default() + } + + #[zbus(property)] + async fn location(&self) -> String { + read_machine_info_field("LOCATION").unwrap_or_default() + } + + #[zbus(property)] + async fn kernel_name(&self) -> String { + nix::sys::utsname::uname() + .ok() + .and_then(|u| u.sysname().to_str().map(String::from)) + .unwrap_or_else(|| "Linux".into()) + } + + #[zbus(property)] + async fn kernel_release(&self) -> String { + nix::sys::utsname::uname() + .ok() + .and_then(|u| u.release().to_str().map(String::from)) + .unwrap_or_default() + } + + #[zbus(property)] + async fn kernel_version(&self) -> String { + nix::sys::utsname::uname() + .ok() + .and_then(|u| u.version().to_str().map(String::from)) + .unwrap_or_default() + } + + #[zbus(property)] + async fn operating_system_pretty_name(&self) -> String { + read_os_release_field("PRETTY_NAME").unwrap_or_else(|| "Linux".into()) + } + + #[zbus(property)] + async fn operating_system_cpename(&self) -> String { + read_os_release_field("CPE_NAME").unwrap_or_default() + } + + #[zbus(property)] + async fn home_url(&self) -> String { + read_os_release_field("HOME_URL").unwrap_or_default() + } + + #[zbus(property)] + async fn hardware_vendor(&self) -> String { + read_dmi("/sys/class/dmi/id/sys_vendor") + } + + #[zbus(property)] + async fn hardware_model(&self) -> String { + read_dmi("/sys/class/dmi/id/product_name") + } + + #[zbus(property)] + async fn firmware_version(&self) -> String { + read_dmi("/sys/class/dmi/id/bios_version") + } + + // ----- Setters: forward al bus interno y guardan en cache ----- + + async fn set_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> { + if !is_valid_hostname(&name) { + return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}"))); + } + // sethostname(2) cambia sólo el running kernel value. + let cstr = std::ffi::CString::new(name.clone()) + .map_err(|e| fdo::Error::Failed(format!("CString: {e}")))?; + let r = unsafe { libc::sethostname(cstr.as_ptr(), name.len()) }; + if r != 0 { + warn!(error = %std::io::Error::last_os_error(), %name, "sethostname syscall falló (¿CAP_SYS_ADMIN?)"); + // No es fatal — guardamos transient para que el property lea el valor nuevo. + } + *self.transient_hostname.lock().unwrap() = Some(name.clone()); + info!(%name, "SetHostname aplicado"); + Ok(()) + } + + async fn set_static_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> { + if !is_valid_hostname(&name) { + return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}"))); + } + atomic_write("/etc/hostname", format!("{name}\n").as_bytes()) + .map_err(|e| fdo::Error::Failed(format!("write /etc/hostname: {e}")))?; + info!(%name, "SetStaticHostname → /etc/hostname"); + Ok(()) + } + + async fn set_pretty_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> { + update_machine_info("PRETTY_HOSTNAME", &name) + .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?; + info!(%name, "SetPrettyHostname → /etc/machine-info"); + Ok(()) + } + + async fn set_icon_name(&self, name: String, _interactive: bool) -> fdo::Result<()> { + update_machine_info("ICON_NAME", &name) + .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?; + info!(%name, "SetIconName → /etc/machine-info"); + Ok(()) + } + + async fn set_chassis(&self, chassis: String, _interactive: bool) -> fdo::Result<()> { + if !matches!(chassis.as_str(), "desktop"|"laptop"|"server"|"tablet"|"handset"|"watch"|"embedded"|"vm"|"container") { + return Err(fdo::Error::InvalidArgs(format!("chassis inválido: {chassis}"))); + } + update_machine_info("CHASSIS", &chassis) + .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?; + info!(%chassis, "SetChassis → /etc/machine-info"); + Ok(()) + } + + async fn set_deployment(&self, deployment: String, _interactive: bool) -> fdo::Result<()> { + update_machine_info("DEPLOYMENT", &deployment) + .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?; + info!(%deployment, "SetDeployment → /etc/machine-info"); + Ok(()) + } + + async fn set_location(&self, location: String, _interactive: bool) -> fdo::Result<()> { + update_machine_info("LOCATION", &location) + .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?; + info!(%location, "SetLocation → /etc/machine-info"); + Ok(()) + } +} + +// ---------------- helpers ---------------- + +fn gethostname_libc() -> Option { + let mut buf = [0u8; 256]; + let r = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) }; + if r != 0 { return None; } + let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + std::str::from_utf8(&buf[..len]).ok().map(String::from) +} + +fn read_os_release_field(field: &str) -> Option { + parse_kv_file("/etc/os-release", field) +} + +fn read_machine_info_field(field: &str) -> Option { + parse_kv_file("/etc/machine-info", field) +} + +fn parse_kv_file(path: &str, field: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + for line in content.lines() { + if let Some((k, v)) = line.split_once('=') { + if k.trim() == field { + return Some(v.trim().trim_matches('"').to_string()); + } + } + } + None +} + +fn read_dmi(path: &str) -> String { + std::fs::read_to_string(path) + .map(|s| s.trim().to_string()) + .unwrap_or_default() +} + +/// RFC 1123 + extra: ASCII alfanumérico, dash, dot. Longitud 1..253. +/// Rechaza vacíos, espacios, control chars. +fn is_valid_hostname(s: &str) -> bool { + if s.is_empty() || s.len() > 253 { return false; } + s.chars().all(|c| + c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' + ) +} + +/// Escritura atómica via tmp + rename. fsync del directorio para +/// garantizar durabilidad post-crash. Permisos 0644. +fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + let p = std::path::Path::new(path); + if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); } + let tmp = p.with_extension("tmp"); + { + let mut f = std::fs::OpenOptions::new() + .create(true).write(true).truncate(true) + .mode(0o644) + .open(&tmp)?; + f.write_all(content)?; + f.sync_all()?; + } + std::fs::rename(&tmp, p)?; + Ok(()) +} + +/// Lee /etc/machine-info, actualiza/inserta una clave, escribe atómico. +fn update_machine_info(key: &str, value: &str) -> std::io::Result<()> { + let path = "/etc/machine-info"; + let existing = std::fs::read_to_string(path).unwrap_or_default(); + let mut found = false; + let mut out = String::new(); + for line in existing.lines() { + if let Some((k, _)) = line.split_once('=') { + if k.trim() == key { + out.push_str(&format!("{key}={value}\n")); + found = true; + continue; + } + } + out.push_str(line); + out.push('\n'); + } + if !found { + out.push_str(&format!("{key}={value}\n")); + } + atomic_write(path, out.as_bytes()) +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Endpoint { + interface: ente_card::InterfaceId([0xa0; 16]), + version: 1, + }], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_hostnamed_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-journald-compat/Cargo.toml b/crates/core/ente-journald-compat/Cargo.toml new file mode 100644 index 0000000..c248dfe --- /dev/null +++ b/crates/core/ente-journald-compat/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ente-journald-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-journald-compat" +path = "src/main.rs" + +[[bin]] +name = "ente-journalctl" +path = "src/journalctl.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +ente-cas = { path = "../ente-cas" } +nix = { workspace = true } +libc = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/core/ente-journald-compat/src/journalctl.rs b/crates/core/ente-journald-compat/src/journalctl.rs new file mode 100644 index 0000000..8164bea --- /dev/null +++ b/crates/core/ente-journald-compat/src/journalctl.rs @@ -0,0 +1,289 @@ +//! ente-journalctl: query CLI sobre el journal persistido en CAS. +//! +//! Lee el index `~/.local/share/ente/journal/index.log` (líneas +//! `timestamp_ms:source:unit:sha_hex`), filtra, y para cada match +//! restituye el blob desde CAS y lo imprime. +//! +//! Uso: +//! ente-journalctl # todo el journal +//! ente-journalctl --unit foo.service # filtra por unit +//! ente-journalctl --since 60 # últimos 60 segundos +//! ente-journalctl --grep "panic" # contiene "panic" +//! ente-journalctl --tail 20 # últimas 20 entries +//! ente-journalctl --json # output JSON-lines + +use std::path::PathBuf; + +struct Args { + unit: Option, + since_secs: Option, + grep: Option, + tail: Option, + source: Option, + output: OutputFormat, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OutputFormat { + Pretty, + Json, + /// systemd journal export format: `KEY=value\n` por field, blank line + /// entre entries. Documented at https://systemd.io/JOURNAL_EXPORT_FORMATS/ + /// Compatible con `journalctl --input-format=export`. + Export, +} + +fn parse_args() -> Args { + let mut args = std::env::args().skip(1); + let mut a = Args { + unit: None, since_secs: None, grep: None, tail: None, + source: None, output: OutputFormat::Pretty, + }; + while let Some(arg) = args.next() { + match arg.as_str() { + "--unit" | "-u" => a.unit = args.next(), + "--since" | "-S" => a.since_secs = args.next().and_then(|s| s.parse().ok()), + "--grep" | "-g" => a.grep = args.next(), + "--tail" | "-n" => a.tail = args.next().and_then(|s| s.parse().ok()), + "--source" => a.source = args.next(), + "--json" => a.output = OutputFormat::Json, + "--output" | "-o" => { + a.output = match args.next().as_deref() { + Some("pretty") | None => OutputFormat::Pretty, + Some("json") | Some("json-lines") => OutputFormat::Json, + Some("export") => OutputFormat::Export, + Some(other) => { + eprintln!("output desconocido: {other}"); + eprintln!("válidos: pretty | json | export"); + std::process::exit(2); + } + }; + } + "-h" | "--help" => { print_help(); std::process::exit(0); } + other => { + eprintln!("argumento desconocido: {other}"); + print_help(); + std::process::exit(2); + } + } + } + a +} + +fn print_help() { + eprintln!("ente-journalctl — query CLI del journal persistido en CAS"); + eprintln!(); + eprintln!("Filtros:"); + eprintln!(" --unit, -u Filtra por unidad (e.g. foo.service)"); + eprintln!(" --source journal | syslog"); + eprintln!(" --since, -S Sólo últimos N segundos"); + eprintln!(" --grep, -g Contiene en el body decoded"); + eprintln!(" --tail, -n Últimas N entries"); + eprintln!("Output:"); + eprintln!(" --output, -o pretty | json | export (systemd journal export)"); + eprintln!(" --json alias de --output json"); +} + +fn index_path() -> PathBuf { + let base = if let Ok(d) = std::env::var("XDG_DATA_HOME") { d } + else if let Ok(h) = std::env::var("HOME") { format!("{h}/.local/share") } + else { "/var/lib".into() }; + PathBuf::from(base).join("ente").join("journal").join("index.log") +} + +fn now_ms() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) +} + +#[derive(Debug)] +struct IndexEntry { + timestamp_ms: u128, + source: String, + unit: String, + sha_hex: String, +} + +fn parse_line(line: &str) -> Option { + let mut parts = line.splitn(4, ':'); + let ts: u128 = parts.next()?.parse().ok()?; + let source = parts.next()?.to_string(); + let unit = parts.next()?.to_string(); + let sha = parts.next()?.to_string(); + if sha.len() != 64 { return None; } + Some(IndexEntry { timestamp_ms: ts, source, unit, sha_hex: sha }) +} + +fn parse_sha(hex: &str) -> Option<[u8; 32]> { + if hex.len() != 64 { return None; } + let mut sha = [0u8; 32]; + for i in 0..32 { + sha[i] = u8::from_str_radix(&hex[i*2..i*2+2], 16).ok()?; + } + Some(sha) +} + +fn main() -> anyhow::Result<()> { + let args = parse_args(); + let path = index_path(); + if !path.exists() { + eprintln!("index no existe: {} — ¿journald-compat ha corrido?", path.display()); + std::process::exit(1); + } + let raw = std::fs::read_to_string(&path)?; + let mut entries: Vec = raw.lines() + .filter_map(parse_line) + .collect(); + + // Filtros + let now = now_ms(); + if let Some(secs) = args.since_secs { + let cutoff = now.saturating_sub(secs as u128 * 1000); + entries.retain(|e| e.timestamp_ms >= cutoff); + } + if let Some(unit) = &args.unit { + entries.retain(|e| &e.unit == unit); + } + if let Some(src) = &args.source { + entries.retain(|e| &e.source == src); + } + // tail después de filtros temporales/identidad pero antes de grep — + // grep es post porque requiere cargar bytes del CAS. + + let mut out: Vec<(IndexEntry, String)> = entries.into_iter() + .filter_map(|e| { + let sha = parse_sha(&e.sha_hex)?; + let bytes = ente_cas::resolve(&sha).ok()?; + let body = String::from_utf8_lossy(&bytes).into_owned(); + Some((e, body)) + }) + .collect(); + + if let Some(g) = &args.grep { + out.retain(|(_, body)| body.contains(g.as_str())); + } + if let Some(n) = args.tail { + let len = out.len(); + if len > n { out.drain(..len - n); } + } + + for (e, body) in out { + match args.output { + OutputFormat::Pretty => print_pretty(&e, &body), + OutputFormat::Json => print_json(&e, &body), + OutputFormat::Export => print_export(&e, &body), + } + } + Ok(()) +} + +fn print_pretty(e: &IndexEntry, body: &str) { + let secs = e.timestamp_ms / 1000; + let ms = e.timestamp_ms % 1000; + let header = if e.unit == "-" { + format!("{}.{:03} [{}]", secs, ms, e.source) + } else { + format!("{}.{:03} [{}] {{{}}}", secs, ms, e.source, e.unit) + }; + println!("{header}"); + // Si es journald native (KEY=value lines), extraer MESSAGE. + if body.contains('=') && body.lines().any(|l| l.contains('=')) { + for line in body.lines() { + if let Some((k, v)) = line.split_once('=') { + if k.trim() == "MESSAGE" { + println!(" {v}"); + return; + } + } + } + } + for line in body.trim_end().lines() { + println!(" {line}"); + } +} + +/// systemd journal export format. Cada entry es un bloque de líneas +/// `KEY=value\n` separado por blank line. Para values con newlines o +/// bytes binarios, el formato usa una variante con length-prefix +/// (8 bytes LE u64) — por simplicidad sólo emitimos values con texto +/// que no contienen newlines o caracteres no-printables. Extraemos +/// MESSAGE/PRIORITY/_SYSTEMD_UNIT del body si es journald native. +/// +/// Compatible con `journalctl --input-format=export -m`. +fn print_export(e: &IndexEntry, body: &str) { + // Timestamps: __REALTIME_TIMESTAMP en µs, __MONOTONIC_TIMESTAMP también. + let realtime_us = e.timestamp_ms.saturating_mul(1000); + println!("__CURSOR=s={};t={};x={}", + &e.sha_hex[..16], // pseudo-cursor: prefix del SHA + realtime_us, + &e.sha_hex[..8]); + println!("__REALTIME_TIMESTAMP={}", realtime_us); + println!("__MONOTONIC_TIMESTAMP={}", realtime_us); + + let host = gethostname_safe(); + if !host.is_empty() { + println!("_HOSTNAME={host}"); + } + + if e.unit != "-" { + println!("_SYSTEMD_UNIT={}", e.unit); + } + println!("_TRANSPORT={}", match e.source.as_str() { + "syslog" => "syslog", + "journal" => "journal", + _ => "stdout", + }); + + // Si el body es journald native (KEY=value lines), emitir cada uno + // verbatim — son los fields originales del producer. Filtrar líneas + // que no son seguras para export (con newlines en value, etc). + if body.contains('=') && body.lines().any(|l| l.contains('=')) { + for line in body.lines() { + if line.contains('=') && line.bytes().all(safe_export_byte) { + println!("{line}"); + } + } + } else { + // Syslog text — empaquetar como MESSAGE. + let msg = body.trim_end() + .replace('\n', " "); // collapsa newlines + if msg.bytes().all(safe_export_byte) { + println!("MESSAGE={msg}"); + } + } + // Blank line separa entries. + println!(); +} + +fn safe_export_byte(b: u8) -> bool { + // ASCII printable, espacio, tab. No newlines (manejados aparte). + (0x20..=0x7E).contains(&b) || b == b'\t' +} + +fn gethostname_safe() -> String { + let mut buf = [0u8; 256]; + let r = unsafe { + libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) + }; + if r != 0 { return String::new(); } + let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + std::str::from_utf8(&buf[..len]).unwrap_or("").to_string() +} + +fn print_json(e: &IndexEntry, body: &str) { + // JSON-lines básico, sin dependencia de serde — formato simple y estable. + let escaped_body = body + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t"); + let unit_field = if e.unit == "-" { "null".to_string() } + else { format!("\"{}\"", e.unit) }; + println!( + r#"{{"timestamp_ms":{},"source":"{}","unit":{},"sha":"{}","body":"{}"}}"#, + e.timestamp_ms, e.source, unit_field, e.sha_hex, escaped_body + ); +} diff --git a/crates/core/ente-journald-compat/src/main.rs b/crates/core/ente-journald-compat/src/main.rs new file mode 100644 index 0000000..620cfd2 --- /dev/null +++ b/crates/core/ente-journald-compat/src/main.rs @@ -0,0 +1,218 @@ +//! ente-journald-compat: stub que absorbe escrituras al journal socket. +//! +//! Listen en `/run/systemd/journal/socket` (datagram) — todo lo que llega +//! se decodifica best-effort y se emite como tracing event. +//! +//! Sin esto, apps que usan `sd_journal_send` o syslog fallan al escribir. +//! Para una implementación real: persistir a CAS por timestamp+sha, +//! exponer query API, indexar por unidad/usuario. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::os::fd::{AsRawFd, OwnedFd}; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use tokio::io::unix::AsyncFd; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{debug, info, warn}; +use tracing_subscriber::EnvFilter; + +const JOURNAL_SOCKET: &str = "/run/systemd/journal/socket"; +const DEV_LOG: &str = "/dev/log"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-journald-compat: arrancando"); + announce_to_fractal().await; + + // Intentamos vincular ambos sockets. Cada uno puede fallar + // independientemente; si alguno funciona, seguimos. + let mut bound = 0usize; + if let Some(stream) = bind_dgram(JOURNAL_SOCKET) { + bound += 1; + spawn_listener(stream, "journal"); + } else { + warn!(path = JOURNAL_SOCKET, "no se pudo bind — necesita CAP_NET_BIND_SERVICE o /run writable"); + } + if let Some(stream) = bind_dgram(DEV_LOG) { + bound += 1; + spawn_listener(stream, "syslog"); + } else { + warn!(path = DEV_LOG, "no se pudo bind /dev/log"); + } + + if bound == 0 { + warn!("ningún socket bound — modo idle"); + } else { + info!(sockets_bound = bound, "journald-compat listening"); + } + + wait_for_term().await +} + +fn bind_dgram(path: &str) -> Option> { + use nix::sys::socket::{bind, socket, AddressFamily, SockFlag, SockType, UnixAddr}; + let _ = std::fs::remove_file(path); + if let Some(parent) = Path::new(path).parent() { + let _ = std::fs::create_dir_all(parent); + } + let fd = match socket( + AddressFamily::Unix, + SockType::Datagram, + SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC, + None, + ) { + Ok(f) => f, + Err(e) => { warn!(?e, "socket() falló"); return None; } + }; + let addr = match UnixAddr::new(path) { + Ok(a) => a, + Err(e) => { warn!(?e, "UnixAddr falló"); return None; } + }; + if let Err(e) = bind(fd.as_raw_fd(), &addr) { + warn!(?e, %path, "bind falló"); + return None; + } + AsyncFd::new(OwnedFdWrap(fd)).ok() +} + +struct OwnedFdWrap(OwnedFd); +impl AsRawFd for OwnedFdWrap { + fn as_raw_fd(&self) -> std::os::fd::RawFd { self.0.as_raw_fd() } +} + +fn spawn_listener(async_fd: AsyncFd, source: &'static str) { + tokio::spawn(async move { + let mut buf = vec![0u8; 64 * 1024]; + loop { + let mut guard = match async_fd.readable().await { + Ok(g) => g, + Err(e) => { warn!(?e, source, "readable failed"); return; } + }; + let raw_fd = guard.get_inner().as_raw_fd(); + loop { + let n = unsafe { + libc::recv(raw_fd, buf.as_mut_ptr() as *mut _, buf.len(), 0) + }; + if n <= 0 { break; } + handle_message(&buf[..n as usize], source); + } + guard.clear_ready(); + } + }); +} + +/// Mutex sobre el archivo index para escrituras concurrentes desde +/// múltiples listeners (journal + syslog). +static INDEX_FILE: Mutex<()> = Mutex::new(()); + +/// Path del index file: `$XDG_DATA_HOME/ente/journal/index.log` (default +/// `~/.local/share/ente/journal/index.log`). +fn index_path() -> PathBuf { + let base = if let Ok(d) = std::env::var("XDG_DATA_HOME") { d } + else if let Ok(h) = std::env::var("HOME") { format!("{h}/.local/share") } + else { "/var/lib".into() }; + PathBuf::from(base).join("ente").join("journal").join("index.log") +} + +fn now_ms() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) +} + +/// Persiste el blob crudo al CAS y appendea una línea al index: +/// `:::`. Errores se logean pero +/// no abortan — perder un mensaje no debe romper journald. +fn persist_to_cas(buf: &[u8], source: &'static str, unit: Option<&str>) { + let sha = match ente_cas::store(buf) { + Ok(s) => s, + Err(e) => { warn!(?e, "CAS store falló"); return; } + }; + let line = format!( + "{}:{}:{}:{}\n", + now_ms(), source, unit.unwrap_or("-"), ente_cas::hex(&sha) + ); + let path = index_path(); + let _guard = INDEX_FILE.lock().unwrap(); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + use std::io::Write; + let mut f = match std::fs::OpenOptions::new() + .create(true).append(true) + .open(&path) + { + Ok(f) => f, + Err(e) => { warn!(?e, path = %path.display(), "abrir index"); return; } + }; + if let Err(e) = f.write_all(line.as_bytes()) { + warn!(?e, "write index"); + } +} + +/// Decodifica best-effort. Formato journald nativo: lines de "KEY=value" +/// (binario para values con newlines, pero raro). Formato syslog: texto +/// con prefijo "tag: message". +fn handle_message(buf: &[u8], source: &'static str) { + if let Ok(s) = std::str::from_utf8(buf) { + if s.contains('=') && s.lines().any(|l| l.contains('=')) { + let mut message = None; + let mut priority = None; + let mut unit: Option = None; + for line in s.lines() { + if let Some((k, v)) = line.split_once('=') { + match k { + "MESSAGE" => message = Some(v.to_string()), + "PRIORITY" => priority = Some(v.to_string()), + "_SYSTEMD_UNIT" | "UNIT" => unit = Some(v.to_string()), + _ => {} + } + } + } + persist_to_cas(buf, source, unit.as_deref()); + if let Some(msg) = message { + info!(target: "journal", source, ?priority, ?unit, "{msg}"); + } else { + debug!(source, len = buf.len(), "journal native sin MESSAGE"); + } + } else { + persist_to_cas(buf, source, None); + info!(target: "syslog", source, "{}", s.trim_end()); + } + } else { + persist_to_cas(buf, source, None); + debug!(source, len = buf.len(), "journal binario (no UTF-8)"); + } +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Journal], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_journald_compat=info,journal=info,syslog=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-kernel/Cargo.toml b/crates/core/ente-kernel/Cargo.toml new file mode 100644 index 0000000..0edbc9a --- /dev/null +++ b/crates/core/ente-kernel/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ente-kernel" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +ente-card = { path = "../ente-card" } +nix = { workspace = true } +libc = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/core/ente-kernel/src/lib.rs b/crates/core/ente-kernel/src/lib.rs new file mode 100644 index 0000000..e849767 --- /dev/null +++ b/crates/core/ente-kernel/src/lib.rs @@ -0,0 +1,11 @@ +//! ente-kernel: primitivas de Linux que el Init usa pero que son reusables +//! desde tools/tests/sub-supervisores. Sin estado global. Cada función es +//! independiente y se puede testear de forma aislada. + +pub mod sigchld; +pub mod surface; +pub mod uevent; + +pub use sigchld::spawn_sigchld_stream; +pub use surface::{become_child_subreaper, bootstrap_kernel_surface}; +pub use uevent::{spawn_uevent_stream, UAction, UEvent}; diff --git a/crates/core/ente-kernel/src/sigchld.rs b/crates/core/ente-kernel/src/sigchld.rs new file mode 100644 index 0000000..e18ec66 --- /dev/null +++ b/crates/core/ente-kernel/src/sigchld.rs @@ -0,0 +1,66 @@ +//! SIGCHLD vía signalfd, no signal handler. +//! +//! Los handlers async-signal sólo pueden invocar funciones async-signal-safe +//! — no allocator, no `mpsc::send`. Con signalfd la señal entra al runtime de +//! Tokio como un `fd` legible y la cosechamos en el bucle como cualquier otro +//! evento. Esto es lo que hace que un init en Rust moderno sea sano. + +use anyhow::Context; +use nix::sys::signal::Signal; +use nix::sys::signalfd::{SfdFlags, SigSet, SignalFd}; +use std::os::fd::AsRawFd; +use tokio::io::unix::AsyncFd; +use tokio::sync::mpsc; +use tracing::{error, trace}; + +/// Bloquea SIGCHLD para entrega asíncrona, abre signalfd, y emite un `()` +/// en el canal cada vez que llega al menos una señal. +pub fn spawn_sigchld_stream() -> anyhow::Result> { + let mut mask = SigSet::empty(); + mask.add(Signal::SIGCHLD); + mask.thread_block().context("SIGCHLD thread_block")?; + + let sfd = SignalFd::with_flags(&mask, SfdFlags::SFD_NONBLOCK | SfdFlags::SFD_CLOEXEC) + .context("signalfd creation")?; + + let async_fd = AsyncFd::new(SignalFdHandle(sfd)).context("AsyncFd::new")?; + + let (tx, rx) = mpsc::channel(8); + tokio::spawn(async move { + loop { + let mut guard = match async_fd.readable().await { + Ok(g) => g, + Err(e) => { error!(?e, "signalfd readable failed"); return; } + }; + // Drenamos todas las siginfos pendientes; signalfd las coalesce + // pero no las cuenta — un read por evento legible es suficiente. + drain(guard.get_inner()); + guard.clear_ready(); + if tx.send(()).await.is_err() { return; } + trace!("SIGCHLD batch coalesced"); + } + }); + + Ok(rx) +} + +struct SignalFdHandle(SignalFd); + +impl AsRawFd for SignalFdHandle { + fn as_raw_fd(&self) -> std::os::fd::RawFd { + self.0.as_raw_fd() + } +} + +fn drain(handle: &SignalFdHandle) { + let fd = handle.as_raw_fd(); + // Tamaño exacto de signalfd_siginfo. Leemos en bucle hasta EAGAIN. + let mut buf = [0u8; std::mem::size_of::()]; + loop { + let n = unsafe { + libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) + }; + if n < 0 { return; } + if n == 0 { return; } + } +} diff --git a/crates/core/ente-kernel/src/surface.rs b/crates/core/ente-kernel/src/surface.rs new file mode 100644 index 0000000..b4b2ff7 --- /dev/null +++ b/crates/core/ente-kernel/src/surface.rs @@ -0,0 +1,86 @@ +//! Bootstrap del entorno kernel para PID 1: monta procfs/sysfs/devtmpfs/cgroup2 +//! y registra al proceso como subreaper para adoptar huérfanos. +//! +//! Idempotente: si los puntos de montaje ya existen (initramfs los montó), +//! el segundo mount falla con EBUSY y simplemente lo ignoramos. + +use anyhow::Context; +use nix::mount::{mount, MsFlags}; +use tracing::debug; + +/// Monta los pseudo-filesystems esenciales. Errores benignos (ya montados) +/// se ignoran; errores serios se propagan. +pub fn bootstrap_kernel_surface() -> anyhow::Result<()> { + // Cada uno con sus flags estándar — NOSUID/NOEXEC/NODEV donde aplica. + mount::( + Some("proc"), "/proc", Some("proc"), + MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None, + ).ok(); + mount::( + Some("sysfs"), "/sys", Some("sysfs"), + MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None, + ).ok(); + mount::( + Some("devtmpfs"), "/dev", Some("devtmpfs"), + MsFlags::MS_NOSUID, None, + ).ok(); + mount::( + Some("cgroup2"), "/sys/fs/cgroup", Some("cgroup2"), + MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None, + ).ok(); + debug!("kernel surface bootstrap completo"); + Ok(()) +} + +/// PR_SET_CHILD_SUBREAPER: que adoptemos huérfanos del fractal. +/// +/// En PID 1 esto es redundante (el kernel ya lo hace), pero se deja explícito +/// para que ente-zero corriendo como sub-init en un container mantenga la +/// misma semántica. +pub fn become_child_subreaper() -> anyhow::Result<()> { + let r = unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1u64, 0u64, 0u64, 0u64) }; + if r != 0 { + anyhow::bail!( + "prctl PR_SET_CHILD_SUBREAPER falló: {}", + std::io::Error::last_os_error() + ); + } + Ok(()) +} + +/// Cosechar zombis hasta vaciar la cola de niños muertos. Devuelve los +/// PIDs cosechados con su estado, como tuplas. +pub fn reap_all() -> Vec { + use nix::errno::Errno; + use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; + let mut out = Vec::new(); + loop { + match waitpid(None, Some(WaitPidFlag::WNOHANG)) { + Ok(WaitStatus::Exited(pid, code)) => { + out.push(ReapedChild { pid: pid.as_raw(), status: ReapStatus::Exited(code) }); + } + Ok(WaitStatus::Signaled(pid, sig, _core)) => { + out.push(ReapedChild { pid: pid.as_raw(), status: ReapStatus::Signaled(sig as i32) }); + } + Ok(WaitStatus::StillAlive) => return out, + Err(Errno::ECHILD) => return out, + Err(_) => return out, + Ok(_) => continue, // Stopped/Continued — irrelevantes + } + } + // unreachable, satisface al borrow checker + #[allow(unreachable_code)] + out +} + +#[derive(Debug, Clone)] +pub struct ReapedChild { + pub pid: i32, + pub status: ReapStatus, +} + +#[derive(Debug, Clone)] +pub enum ReapStatus { + Exited(i32), + Signaled(i32), +} diff --git a/crates/core/ente-kernel/src/uevent.rs b/crates/core/ente-kernel/src/uevent.rs new file mode 100644 index 0000000..6e8f0fd --- /dev/null +++ b/crates/core/ente-kernel/src/uevent.rs @@ -0,0 +1,146 @@ +//! Stream de uevents del kernel vía NETLINK_KOBJECT_UEVENT. +//! +//! Bind requiere CAP_NET_ADMIN. En dev mode normal eso no está disponible — +//! el caller debe estar preparado para que `spawn_uevent_stream` falle, y +//! continuar sin grafo de dispositivos. + +use anyhow::Context; +use ente_card::DeviceClass; +use nix::sys::socket::{bind, socket, AddressFamily, NetlinkAddr, SockFlag, SockProtocol, SockType}; +use std::collections::HashMap; +use std::os::fd::{AsRawFd, OwnedFd}; +use tokio::io::unix::AsyncFd; +use tokio::sync::mpsc; +use tracing::{trace, warn}; + +#[derive(Debug, Clone)] +pub struct UEvent { + pub action: UAction, + pub devpath: String, + pub subsystem: Option, + pub device_class: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UAction { + Add, Remove, Change, Move, Online, Offline, Bind, Unbind, Unknown, +} + +pub fn spawn_uevent_stream() -> anyhow::Result> { + let fd: OwnedFd = socket( + AddressFamily::Netlink, + SockType::Datagram, + SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC, + SockProtocol::NetlinkKObjectUEvent, + ).context("netlink socket")?; + + // pid=0 → kernel asigna; groups=1 → multicast group del kernel uevent. + let addr = NetlinkAddr::new(0, 1); + bind(fd.as_raw_fd(), &addr).context("bind netlink uevent (CAP_NET_ADMIN?)")?; + + let async_fd = AsyncFd::new(NetlinkHandle(fd)).context("AsyncFd::new(netlink)")?; + let (tx, rx) = mpsc::channel(128); + + tokio::spawn(async move { + let mut buf = vec![0u8; 16 * 1024]; + loop { + let mut guard = match async_fd.readable().await { + Ok(g) => g, + Err(e) => { warn!(?e, "netlink readable"); return; } + }; + let raw_fd = guard.get_inner().as_raw_fd(); + loop { + let n = unsafe { + libc::recv(raw_fd, buf.as_mut_ptr() as *mut _, buf.len(), 0) + }; + if n <= 0 { break; } + if let Some(evt) = parse_uevent(&buf[..n as usize]) { + trace!(?evt.action, devpath = %evt.devpath, "uevent"); + if tx.send(evt).await.is_err() { return; } + } + } + guard.clear_ready(); + } + }); + + Ok(rx) +} + +struct NetlinkHandle(OwnedFd); +impl AsRawFd for NetlinkHandle { + fn as_raw_fd(&self) -> std::os::fd::RawFd { self.0.as_raw_fd() } +} + +fn parse_uevent(buf: &[u8]) -> Option { + // udev re-emite mensajes con magic "libudev\0..." al multicast group 2. + // Si llegan al grupo 1 (improbable con bind groups=1) los filtramos igual. + if buf.starts_with(b"libudev\0") { + return None; + } + let mut parts = buf.split(|b| *b == 0).filter(|s| !s.is_empty()); + let header = std::str::from_utf8(parts.next()?).ok()?; + let (action_s, devpath) = header.split_once('@')?; + let mut env: HashMap = HashMap::new(); + for kv in parts { + if let Ok(s) = std::str::from_utf8(kv) { + if let Some((k, v)) = s.split_once('=') { + env.insert(k.to_string(), v.to_string()); + } + } + } + let subsystem = env.remove("SUBSYSTEM"); + let device_class = subsystem.as_deref().and_then(map_device_class); + Some(UEvent { + action: parse_action(action_s), + devpath: devpath.to_string(), + subsystem, + device_class, + }) +} + +fn parse_action(s: &str) -> UAction { + match s { + "add" => UAction::Add, + "remove" => UAction::Remove, + "change" => UAction::Change, + "move" => UAction::Move, + "online" => UAction::Online, + "offline" => UAction::Offline, + "bind" => UAction::Bind, + "unbind" => UAction::Unbind, + _ => UAction::Unknown, + } +} + +fn map_device_class(subsys: &str) -> Option { + Some(match subsys { + "block" => DeviceClass::Block, + "tty" => DeviceClass::Tty, + "input" => DeviceClass::Input, + "drm" => DeviceClass::Drm, + "net" => DeviceClass::Net, + "hidraw" => DeviceClass::Hidraw, + _ => return None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_minimal_uevent() { + let buf = b"add@/devices/foo\0ACTION=add\0DEVPATH=/devices/foo\0SUBSYSTEM=block\0"; + let evt = parse_uevent(buf).expect("parsed"); + assert_eq!(evt.action, UAction::Add); + assert_eq!(evt.devpath, "/devices/foo"); + assert_eq!(evt.subsystem.as_deref(), Some("block")); + assert!(matches!(evt.device_class, Some(DeviceClass::Block))); + } + + #[test] + fn libudev_messages_filtered() { + let buf = b"libudev\0\xfe\xed\xca\xfeadd@/devices/foo\0"; + assert!(parse_uevent(buf).is_none()); + } +} diff --git a/crates/core/ente-localed-compat/Cargo.toml b/crates/core/ente-localed-compat/Cargo.toml new file mode 100644 index 0000000..04a5c5b --- /dev/null +++ b/crates/core/ente-localed-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-localed-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-localed-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/core/ente-localed-compat/src/main.rs b/crates/core/ente-localed-compat/src/main.rs new file mode 100644 index 0000000..f553c54 --- /dev/null +++ b/crates/core/ente-localed-compat/src/main.rs @@ -0,0 +1,219 @@ +//! ente-localed-compat: shim de `org.freedesktop.locale1`. +//! +//! GNOME settings panel "Region & Language" llama aquí. Properties leen +//! /etc/locale.conf y /etc/vconsole.conf; setters log + forward. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::sync::Mutex; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface}; + +const BUS_NAME: &str = "org.freedesktop.locale1"; +const OBJ_PATH: &str = "/org/freedesktop/locale1"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-localed-compat: arrancando"); + announce_to_fractal().await; + + let manager = LocaleManager::default(); + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { + warn!(?e, "build conn falló — modo idle"); + wait_for_term().await + } + }, + Err(e) => { + warn!(?e, "builder D-Bus falló — modo idle"); + wait_for_term().await + } + } +} + +#[derive(Default)] +struct LocaleManager { + transient_locale: Mutex>>, +} + +#[interface(name = "org.freedesktop.locale1")] +impl LocaleManager { + /// Locale actual como array de "KEY=value" (LANG=en_US.UTF-8, LC_TIME=...). + /// Default: leer /etc/locale.conf. + #[zbus(property)] + async fn locale(&self) -> Vec { + if let Some(v) = self.transient_locale.lock().unwrap().clone() { + return v; + } + match std::fs::read_to_string("/etc/locale.conf") { + Ok(c) => c.lines() + .filter(|l| !l.trim().is_empty() && !l.starts_with('#')) + .map(|s| s.trim().to_string()) + .collect(), + Err(_) => vec!["LANG=C.UTF-8".into()], + } + } + + #[zbus(property)] + async fn x11layout(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbLayout").unwrap_or_default() + } + + #[zbus(property)] + async fn x11model(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbModel").unwrap_or_default() + } + + #[zbus(property)] + async fn x11variant(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbVariant").unwrap_or_default() + } + + #[zbus(property)] + async fn x11options(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbOptions").unwrap_or_default() + } + + #[zbus(property)] + async fn vconsole_keymap(&self) -> String { + read_vconsole("KEYMAP").unwrap_or_default() + } + + #[zbus(property)] + async fn vconsole_keymap_toggle(&self) -> String { + read_vconsole("KEYMAP_TOGGLE").unwrap_or_default() + } + + async fn set_locale(&self, locale: Vec, _interactive: bool) -> fdo::Result<()> { + // Validar formato KEY=value en cada entry. + for entry in &locale { + if !entry.contains('=') { + return Err(fdo::Error::InvalidArgs( + format!("locale entry inválido (sin '='): {entry}") + )); + } + } + let content: String = locale.iter() + .map(|s| format!("{s}\n")) + .collect(); + atomic_write("/etc/locale.conf", content.as_bytes()) + .map_err(|e| fdo::Error::Failed(format!("write /etc/locale.conf: {e}")))?; + *self.transient_locale.lock().unwrap() = Some(locale.clone()); + info!(?locale, "SetLocale → /etc/locale.conf"); + Ok(()) + } + + async fn set_vconsole_keymap( + &self, + keymap: String, + keymap_toggle: String, + _convert: bool, + _interactive: bool, + ) -> fdo::Result<()> { + info!(%keymap, %keymap_toggle, "SetVConsoleKeymap (stub)"); + Ok(()) + } + + async fn set_x11_keyboard( + &self, + layout: String, + model: String, + variant: String, + options: String, + _convert: bool, + _interactive: bool, + ) -> fdo::Result<()> { + info!(%layout, %model, %variant, %options, "SetX11Keyboard (stub)"); + Ok(()) + } +} + +fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + let p = std::path::Path::new(path); + if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); } + let tmp = p.with_extension("tmp"); + { + let mut f = std::fs::OpenOptions::new() + .create(true).write(true).truncate(true) + .mode(0o644) + .open(&tmp)?; + f.write_all(content)?; + f.sync_all()?; + } + std::fs::rename(&tmp, p)?; + Ok(()) +} + +fn read_kv(path: &str, key: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with(&format!("Option \"{key}\"")) || trimmed.starts_with(key) { + // Best-effort parse: tomar lo que está entre comillas. + if let Some(start) = trimmed.find('"') { + let rest = &trimmed[start + 1..]; + if let Some(end) = rest.find('"') { + return Some(rest[..end].to_string()); + } + } + } + } + None +} + +fn read_vconsole(key: &str) -> Option { + let content = std::fs::read_to_string("/etc/vconsole.conf").ok()?; + for line in content.lines() { + if let Some((k, v)) = line.split_once('=') { + if k.trim() == key { + return Some(v.trim().trim_matches('"').to_string()); + } + } + } + None +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Endpoint { + interface: ente_card::InterfaceId([0xa2; 16]), + version: 1, + }], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_localed_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-logind-compat/Cargo.toml b/crates/core/ente-logind-compat/Cargo.toml new file mode 100644 index 0000000..3a4f883 --- /dev/null +++ b/crates/core/ente-logind-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-logind-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-logind-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/core/ente-logind-compat/src/main.rs b/crates/core/ente-logind-compat/src/main.rs new file mode 100644 index 0000000..ce1736f --- /dev/null +++ b/crates/core/ente-logind-compat/src/main.rs @@ -0,0 +1,249 @@ +//! ente-logind-compat: el Ente que se hace pasar por systemd-logind. +//! +//! Vive FUERA de PID 1 — un parser D-Bus en el Init es una bomba con CVEs +//! históricos. Ejecutado como hijo del Ente #0 con `Restart` supervision. +//! +//! Implementa el subset del interface `org.freedesktop.login1.Manager` que +//! GNOME/KDE consultan en arranque. Cada método se traduce internamente en +//! una request al bus interno del fractal — capacidades tipadas, no nombres +//! D-Bus opacos hacia abajo. +//! +//! ## Lo que GNOME/KDE realmente llaman al boot +//! - ListSessions, ListUsers, GetSession* +//! - Inhibit (mantiene un fd vivo mientras la app está activa) +//! - CanPowerOff/CanReboot/CanSuspend +//! - PowerOff/Reboot/Suspend +//! - Properties: IdleHint, Docked, etc. +//! +//! El stub responde "no hay sesiones" y "sí puedo apagar" — suficiente para +//! que GNOME complete arranque sin marcar fallo. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::time::Duration; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface, zvariant::OwnedObjectPath, Connection}; + +const BUS_NAME: &str = "org.freedesktop.login1"; +const MANAGER_PATH: &str = "/org/freedesktop/login1"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-logind-compat: arrancando"); + + let bus_addr = std::env::var("DBUS_SYSTEM_BUS_ADDRESS") + .unwrap_or_else(|_| "unix:path=/var/run/dbus/system_bus_socket".into()); + let bus_path = bus_addr.strip_prefix("unix:path=").unwrap_or(&bus_addr); + let bus_present = std::path::Path::new(bus_path).exists(); + info!(bus_addr, bus_present, "configuración D-Bus"); + + // Anunciamos nuestra presencia al bus interno del fractal antes de + // intentar registrar el nombre D-Bus. Esto sirve como handshake "estoy + // vivo" independiente del estado del system bus. + announce_to_fractal().await; + + if !bus_present { + warn!("system bus no disponible — modo idle (esperando SIGTERM)"); + return wait_for_term().await; + } + + let conn = match build_connection().await { + Ok(c) => c, + Err(e) => { + warn!(?e, "fallo al registrar org.freedesktop.login1 — modo idle"); + // No retornamos error: la supervisión Restart entraría en bucle + // si systemd-logind real ya posee el nombre. Esperar señal y salir. + return wait_for_term().await; + } + }; + + info!("logind compat corriendo — esperando señales"); + let _ = conn; // mantener viva la conexión hasta SIGTERM + wait_for_term().await +} + +async fn build_connection() -> anyhow::Result { + let manager = LogindManager::default(); + let conn = zbus::connection::Builder::system()? + .name(BUS_NAME)? + .serve_at(MANAGER_PATH, manager)? + .build() + .await?; + info!(name = BUS_NAME, path = MANAGER_PATH, "name acquired + manager served"); + Ok(conn) +} + +async fn announce_to_fractal() { + match BusClient::from_env().await { + Ok(mut client) => { + let req = BusRequest::Announce { + capabilities: vec![Capability::LegacyLogind], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } + Err(e) => warn!(?e, "no se pudo conectar al bus interno"), + } +} + +async fn forward_to_fractal(req: BusRequest) -> fdo::Result<()> { + let mut client = BusClient::from_env().await + .map_err(|e| fdo::Error::Failed(format!("bus client: {e}")))?; + match client.call(req).await { + Ok(BusResponse::Ok) => Ok(()), + Ok(BusResponse::Error(s)) => Err(fdo::Error::Failed(s)), + Ok(other) => Err(fdo::Error::Failed(format!("respuesta inesperada: {other:?}"))), + Err(e) => Err(fdo::Error::Failed(format!("bus call: {e}"))), + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + let mut tick = tokio::time::interval(Duration::from_secs(60)); + tick.tick().await; // descartar el primer tick inmediato + loop { + tokio::select! { + _ = term.recv() => { info!("SIGTERM — cierre ordenado"); return Ok(()); } + _ = int_.recv() => { info!("SIGINT — cierre"); return Ok(()); } + _ = tick.tick() => { info!("heartbeat"); } + } + } +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_logind_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} + +#[derive(Default)] +struct LogindManager { + /// Contador monótono de inhibits. Real impl mantendría una tabla con + /// el fd vivo de cada uno y los enrutaría al bus interno del fractal. + inhibit_counter: AtomicU32, +} + +/// Tipos del wire format de `org.freedesktop.login1.Manager`. +type SessionTuple = (String, u32, String, String, OwnedObjectPath); +type UserTuple = (u32, String, OwnedObjectPath); + +#[interface(name = "org.freedesktop.login1.Manager")] +impl LogindManager { + // ---- Listado / lookup ---- + + async fn list_sessions(&self) -> fdo::Result> { + Ok(vec![]) + } + + async fn list_users(&self) -> fdo::Result> { + Ok(vec![]) + } + + async fn get_session(&self, _session_id: String) -> fdo::Result { + Err(fdo::Error::Failed("no sessions in fractal".into())) + } + + async fn get_session_by_pid(&self, _pid: u32) -> fdo::Result { + Err(fdo::Error::Failed("no sessions in fractal".into())) + } + + async fn get_user(&self, _uid: u32) -> fdo::Result { + Err(fdo::Error::Failed("no users in fractal".into())) + } + + async fn get_user_by_pid(&self, _pid: u32) -> fdo::Result { + Err(fdo::Error::Failed("no users in fractal".into())) + } + + // ---- Inhibit ---- + // + // Real: devuelve un fd que el cliente mantiene abierto mientras quiere + // inhibir. Cuando lo cierra, sabemos que terminó. Aquí: stub que falla + // con NotSupported — GNOME registra warning pero continúa el arranque. + + async fn inhibit( + &self, + what: String, + who: String, + why: String, + mode: String, + ) -> fdo::Result { + let n = self.inhibit_counter.fetch_add(1, Ordering::Relaxed); + info!(n, %what, %who, %why, %mode, "Inhibit (stub)"); + Err(fdo::Error::NotSupported("Inhibit todavía no enruta al bus interno".into())) + } + + // ---- Power management ---- + + async fn power_off(&self, interactive: bool) -> fdo::Result<()> { + info!(interactive, "PowerOff D-Bus → bus interno"); + forward_to_fractal(BusRequest::PowerOff { interactive }).await + } + + async fn reboot(&self, interactive: bool) -> fdo::Result<()> { + info!(interactive, "Reboot D-Bus → bus interno"); + forward_to_fractal(BusRequest::Reboot { interactive }).await + } + + async fn suspend(&self, interactive: bool) -> fdo::Result<()> { + info!(interactive, "Suspend D-Bus → bus interno"); + forward_to_fractal(BusRequest::Suspend { interactive }).await + } + + async fn hibernate(&self, interactive: bool) -> fdo::Result<()> { + info!(interactive, "Hibernate D-Bus → bus interno"); + forward_to_fractal(BusRequest::Hibernate { interactive }).await + } + + async fn can_power_off(&self) -> fdo::Result { + Ok("yes".into()) + } + + async fn can_reboot(&self) -> fdo::Result { + Ok("yes".into()) + } + + async fn can_suspend(&self) -> fdo::Result { + // "challenge" = válido, requiere autenticación. GNOME muestra el + // botón pero pide PIN/contraseña antes de invocar Suspend. + Ok("challenge".into()) + } + + async fn can_hibernate(&self) -> fdo::Result { + Ok("challenge".into()) + } + + // ---- Properties mínimas ---- + + #[zbus(property)] + async fn idle_hint(&self) -> bool { false } + + #[zbus(property)] + async fn idle_since_hint(&self) -> u64 { 0 } + + #[zbus(property)] + async fn idle_since_hint_monotonic(&self) -> u64 { 0 } + + #[zbus(property)] + async fn block_inhibited(&self) -> String { String::new() } + + #[zbus(property)] + async fn delay_inhibited(&self) -> String { String::new() } + + #[zbus(property)] + async fn docked(&self) -> bool { false } + + #[zbus(property)] + async fn lid_closed(&self) -> bool { false } + + #[zbus(property)] + async fn on_external_power(&self) -> bool { true } +} diff --git a/crates/core/ente-machined-compat/Cargo.toml b/crates/core/ente-machined-compat/Cargo.toml new file mode 100644 index 0000000..eb88b4a --- /dev/null +++ b/crates/core/ente-machined-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-machined-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-machined-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/core/ente-machined-compat/src/main.rs b/crates/core/ente-machined-compat/src/main.rs new file mode 100644 index 0000000..4ad7e00 --- /dev/null +++ b/crates/core/ente-machined-compat/src/main.rs @@ -0,0 +1,186 @@ +//! ente-machined-compat: shim de `org.freedesktop.machine1`. +//! +//! systemd-machined trackea VMs y containers (typically managed por systemd-nspawn). +//! En el fractal cada Ente con namespaces es candidato a "machine", pero la +//! correspondencia no es 1:1 — un Ente puede tener menos aislamiento que una +//! container completa. +//! +//! Este shim devuelve listas vacías para no romper clientes (gnome-boxes, +//! virt-manager, etc) que llaman a `ListMachines` durante boot. Métodos de +//! mutación (RegisterMachine, KillMachine) se aceptan como no-op con audit +//! log via tracing. +//! +//! Producción real: integrar con el graph del fractal — ListMachines query +//! BusRequest::ListEntes filtrado por `card.soma.namespaces.pid`. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::collections::HashMap; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface, zvariant::OwnedValue}; + +const BUS_NAME: &str = "org.freedesktop.machine1"; +const OBJ_PATH: &str = "/org/freedesktop/machine1"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-machined-compat: arrancando"); + announce_to_fractal().await; + + let manager = MachineManager; + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { warn!(?e, "build conn falló — modo idle"); wait_for_term().await } + }, + Err(e) => { warn!(?e, "builder D-Bus falló — modo idle"); wait_for_term().await } + } +} + +struct MachineManager; + +/// Tipo del wire format de ListMachines: `(s, s, s, u, ay, ay, t, ay)` — +/// name, class, service, leader_pid, root_directory_path, id_unix, time_obtained, +/// machine_id_bytes. systemd usa este struct simplificado. +type Machine = (String, String, String, u32, String); + +#[interface(name = "org.freedesktop.machine1.Manager")] +impl MachineManager { + /// Lista vacía — no trackeamos containers todavía. + async fn list_machines(&self) -> fdo::Result> { + Ok(vec![]) + } + + /// Devuelve siempre NotFound — sin machines registradas. + async fn get_machine(&self, name: String) -> fdo::Result { + Err(fdo::Error::Failed(format!("machine '{name}' no encontrada"))) + } + + async fn get_machine_by_pid(&self, pid: u32) -> fdo::Result { + Err(fdo::Error::Failed(format!("PID {pid} no asociado a ninguna machine"))) + } + + async fn register_machine( + &self, + name: String, + _id: Vec, + _service: String, + class: String, + _leader_pid: u32, + _root_directory: String, + ) -> fdo::Result { + info!(%name, %class, "RegisterMachine (no-op)"); + Err(fdo::Error::NotSupported( + "RegisterMachine no implementado — usar Cards del fractal".into() + )) + } + + async fn register_machine_with_network( + &self, + name: String, + id: Vec, + service: String, + class: String, + leader_pid: u32, + root_directory: String, + _network_interfaces: Vec, + ) -> fdo::Result { + self.register_machine(name, id, service, class, leader_pid, root_directory).await + } + + async fn create_machine( + &self, + name: String, + _id: Vec, + _service: String, + class: String, + _leader_pid: u32, + _root_directory: String, + _scope_properties: Vec<(String, OwnedValue)>, + ) -> fdo::Result { + info!(%name, %class, "CreateMachine (no-op)"); + Err(fdo::Error::NotSupported( + "CreateMachine no implementado".into() + )) + } + + async fn terminate_machine(&self, name: String) -> fdo::Result<()> { + info!(%name, "TerminateMachine (no-op)"); + Ok(()) + } + + async fn kill_machine(&self, name: String, _who: String, _signal: i32) -> fdo::Result<()> { + info!(%name, "KillMachine (no-op)"); + Ok(()) + } + + async fn get_machine_address(&self, name: String) -> fdo::Result)>> { + warn!(%name, "GetMachineAddress (sin tracking, devuelvo vacío)"); + Ok(vec![]) + } + + async fn get_machine_osrelease(&self, name: String) -> fdo::Result> { + warn!(%name, "GetMachineOSRelease (sin tracking)"); + Ok(HashMap::new()) + } + + /// Operaciones sobre la "host machine" (PID 1 namespace) — siempre + /// disponibles. Usamos el path canónico `/org/freedesktop/machine1/machine/_host`. + async fn open_machine_login(&self, _name: String) -> fdo::Result<(zbus::zvariant::OwnedObjectPath, zbus::zvariant::OwnedFd)> { + Err(fdo::Error::NotSupported( + "OpenMachineLogin no implementado".into() + )) + } + + async fn open_machine_shell( + &self, + _name: String, + _user: String, + _path: String, + _args: Vec, + _environment: Vec, + ) -> fdo::Result<(zbus::zvariant::OwnedObjectPath, zbus::zvariant::OwnedFd)> { + Err(fdo::Error::NotSupported("OpenMachineShell no implementado".into())) + } +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Endpoint { + interface: ente_card::InterfaceId([0xa5; 16]), + version: 1, + }], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_machined_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-notify-compat/Cargo.toml b/crates/core/ente-notify-compat/Cargo.toml new file mode 100644 index 0000000..fdba47a --- /dev/null +++ b/crates/core/ente-notify-compat/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ente-notify-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-notify-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +nix = { workspace = true } +libc = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/core/ente-notify-compat/src/main.rs b/crates/core/ente-notify-compat/src/main.rs new file mode 100644 index 0000000..afcf275 --- /dev/null +++ b/crates/core/ente-notify-compat/src/main.rs @@ -0,0 +1,160 @@ +//! ente-notify-compat: NOTIFY_SOCKET listener para apps `Type=notify`. +//! +//! systemd convention: el servicio escribe `KEY=value\n` lines a un socket +//! datagram cuya path está en `$NOTIFY_SOCKET`. Keys típicos: +//! - READY=1 (servicio listo para recibir requests) +//! - STATUS=text (descripción del estado) +//! - WATCHDOG=1 (heartbeat) +//! - STOPPING=1 (cierre ordenado) +//! - MAINPID= (cambio de PID principal) +//! +//! Path canonical: /run/systemd/notify. Bindeable sólo con CAP_NET_BIND_SERVICE +//! o si /run es writable. +//! +//! Para que las apps lo usen, ente-soma debe inyectar `NOTIFY_SOCKET=` +//! en el envp de cada Ente encarnado. Eso ya lo hace via build_env() — +//! aquí sólo necesitamos que el path sea coherente. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::os::fd::{AsRawFd, OwnedFd}; +use std::path::Path; +use tokio::io::unix::AsyncFd; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{debug, info, warn}; +use tracing_subscriber::EnvFilter; + +const NOTIFY_SOCKET_PATH: &str = "/run/systemd/notify"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!(path = NOTIFY_SOCKET_PATH, "ente-notify-compat: arrancando"); + announce_to_fractal().await; + + let stream = match bind_dgram(NOTIFY_SOCKET_PATH) { + Some(s) => s, + None => { + warn!("no se pudo bind — modo idle (apps Type=notify caerán a no-op)"); + return wait_for_term().await; + } + }; + info!("NOTIFY_SOCKET listening"); + spawn_listener(stream); + wait_for_term().await +} + +fn bind_dgram(path: &str) -> Option> { + use nix::sys::socket::{bind, socket, AddressFamily, SockFlag, SockType, UnixAddr}; + let _ = std::fs::remove_file(path); + if let Some(parent) = Path::new(path).parent() { + let _ = std::fs::create_dir_all(parent); + } + let fd = socket( + AddressFamily::Unix, + SockType::Datagram, + SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC, + None, + ).ok()?; + let addr = UnixAddr::new(path).ok()?; + if let Err(e) = bind(fd.as_raw_fd(), &addr) { + warn!(?e, %path, "bind"); + return None; + } + // Permisos abiertos: cualquier proceso debería poder escribir notificaciones. + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o666)); + AsyncFd::new(OwnedFdWrap(fd)).ok() +} + +struct OwnedFdWrap(OwnedFd); +impl AsRawFd for OwnedFdWrap { + fn as_raw_fd(&self) -> std::os::fd::RawFd { self.0.as_raw_fd() } +} + +fn spawn_listener(async_fd: AsyncFd) { + tokio::spawn(async move { + let mut buf = vec![0u8; 16 * 1024]; + loop { + let mut guard = match async_fd.readable().await { + Ok(g) => g, + Err(e) => { warn!(?e, "readable"); return; } + }; + let raw_fd = guard.get_inner().as_raw_fd(); + loop { + let n = unsafe { libc::recv(raw_fd, buf.as_mut_ptr() as *mut _, buf.len(), 0) }; + if n <= 0 { break; } + handle_notification(&buf[..n as usize]); + } + guard.clear_ready(); + } + }); +} + +fn handle_notification(buf: &[u8]) { + let s = match std::str::from_utf8(buf) { + Ok(s) => s, + Err(_) => { debug!(len = buf.len(), "notify binario, skip"); return; } + }; + let mut ready = false; + let mut status = None; + let mut mainpid = None; + let mut watchdog = false; + let mut stopping = false; + let mut other_keys = Vec::new(); + for line in s.lines() { + if let Some((k, v)) = line.split_once('=') { + match k { + "READY" if v == "1" => ready = true, + "STATUS" => status = Some(v.to_string()), + "MAINPID" => mainpid = v.parse::().ok(), + "WATCHDOG" if v == "1" => watchdog = true, + "STOPPING" if v == "1" => stopping = true, + _ => other_keys.push(format!("{k}={v}")), + } + } + } + if ready { + info!(?status, ?mainpid, "sd_notify READY"); + } else if stopping { + info!(?status, "sd_notify STOPPING"); + } else if watchdog { + debug!("sd_notify WATCHDOG"); + } else if let Some(s) = status { + info!(%s, "sd_notify STATUS"); + } else if !other_keys.is_empty() { + debug!(keys = ?other_keys, "sd_notify (other)"); + } +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Endpoint { + interface: ente_card::InterfaceId([0xa7; 16]), + version: 1, + }], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_notify_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-policy-provider/Cargo.toml b/crates/core/ente-policy-provider/Cargo.toml new file mode 100644 index 0000000..9b2e83c --- /dev/null +++ b/crates/core/ente-policy-provider/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ente-policy-provider" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-policy-provider" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/core/ente-policy-provider/src/main.rs b/crates/core/ente-policy-provider/src/main.rs new file mode 100644 index 0000000..3777ddc --- /dev/null +++ b/crates/core/ente-policy-provider/src/main.rs @@ -0,0 +1,221 @@ +//! ente-policy-provider: Ente que arbitra autorizaciones de Polkit. +//! +//! Se anuncia como proveedor de `POLKIT_DECISION_IFACE` en el bus interno. +//! Cuando `ente-polkit-compat` recibe `CheckAuthorization` D-Bus, forwarda +//! a este Ente vía Invoke. Aquí decidimos sí/no según política configurada. +//! +//! Wire format del blob de entrada: `pid_be_u32 | uid_be_u32 | action_id_utf8`. +//! Respuesta: `[decision_byte]` — 1 = allow, 0 = deny. +//! +//! Política se carga de `/etc/ente/policy.json` (o ruta override por env +//! `ENTE_POLICY_FILE`). Formato: +//! ```json +//! { +//! "default": "allow", +//! "rules": [ +//! { "match": "org.freedesktop.hostname1.*", "decision": "allow" }, +//! { "match": "org.freedesktop.login1.power-off", "require_uid": 0 }, +//! { "match": "*.set-*", "decision": "deny", "audit": true } +//! ] +//! } +//! ``` + +use ente_bus::{BusResponse, BusServer, InvokeHandler, POLKIT_DECISION_IFACE}; +use ente_card::Capability; +use serde::Deserialize; +use std::sync::Arc; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; + +#[derive(Debug, Clone, Deserialize)] +struct PolicyConfig { + #[serde(default = "default_decision")] + default: Decision, + #[serde(default)] + rules: Vec, +} + +fn default_decision() -> Decision { Decision::Allow } + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum Decision { Allow, Deny } + +#[derive(Debug, Clone, Deserialize)] +struct Rule { + /// Glob simple: `*` = wildcard. `org.freedesktop.hostname1.*` matchea + /// cualquier action_id con ese prefijo. + r#match: String, + #[serde(default)] + decision: Option, + /// Si presente, sólo este uid pasa. Otros se denegen. + #[serde(default)] + require_uid: Option, + /// Si presente, sólo este pid pasa. + #[serde(default)] + require_pid: Option, + #[serde(default)] + audit: bool, +} + +impl Default for PolicyConfig { + fn default() -> Self { + // Default sensato: caps escaladas requieren uid 0; el resto allow. + Self { + default: Decision::Allow, + rules: vec![ + // Power management: cualquiera puede pedir el reboot, + // pero la decisión final está en el holder de Capability::Spawn. + Rule { + r#match: "org.freedesktop.login1.set-wall-message".into(), + decision: Some(Decision::Allow), require_uid: None, require_pid: None, audit: true, + }, + // hostname/timezone/locale: requieren root. + Rule { + r#match: "org.freedesktop.hostname1.*".into(), + decision: None, require_uid: Some(0), require_pid: None, audit: true, + }, + Rule { + r#match: "org.freedesktop.timedate1.*".into(), + decision: None, require_uid: Some(0), require_pid: None, audit: true, + }, + Rule { + r#match: "org.freedesktop.locale1.*".into(), + decision: None, require_uid: Some(0), require_pid: None, audit: true, + }, + ], + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-policy-provider: arrancando"); + + let policy = load_policy(); + info!(rules = policy.rules.len(), default = ?policy.default, "policy cargada"); + + let handler = PolicyHandler { policy: Arc::new(policy) }; + + tokio::spawn(async { + let mut term = signal(SignalKind::terminate()).unwrap(); + let mut int_ = signal(SignalKind::interrupt()).unwrap(); + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + std::process::exit(0); + }); + + // Una única conexión: announce + serve. Bidirectional bajo el hood. + let mut server = BusServer::from_env().await?; + server.announce(vec![Capability::Endpoint { + interface: POLKIT_DECISION_IFACE, + version: 1, + }]).await?; + info!("Announce OK; sirviendo invokes de policy decision"); + server.serve(handler).await?; + Ok(()) +} + +struct PolicyHandler { + policy: Arc, +} + +impl InvokeHandler for PolicyHandler { + fn handle(&mut self, cap: Capability, blob: Vec) -> BusResponse { + // Validar cap (defensa contra forwarding a interface incorrecto). + if !matches!(&cap, Capability::Endpoint { interface, .. } if *interface == POLKIT_DECISION_IFACE) { + return BusResponse::Error(format!("policy-provider: cap inesperado {cap:?}")); + } + // Decodificar blob: [pid:4][uid:4][action_id...] + if blob.len() < 8 { + return BusResponse::Error("blob demasiado corto (esperado pid|uid|action_id)".into()); + } + let pid = u32::from_be_bytes(blob[0..4].try_into().unwrap()); + let uid = u32::from_be_bytes(blob[4..8].try_into().unwrap()); + let action_id = match std::str::from_utf8(&blob[8..]) { + Ok(s) => s, + Err(_) => return BusResponse::Error("action_id no es UTF-8".into()), + }; + + let decision = decide(&self.policy, action_id, pid, uid); + let byte = if decision == Decision::Allow { 1u8 } else { 0u8 }; + info!(action_id, pid, uid, ?decision, "policy decision"); + BusResponse::Invoked { result: vec![byte] } + } +} + +fn decide(policy: &PolicyConfig, action_id: &str, pid: u32, uid: u32) -> Decision { + for rule in &policy.rules { + if !glob_match(&rule.r#match, action_id) { continue; } + if let Some(req_uid) = rule.require_uid { + if uid != req_uid { + if rule.audit { + info!(action_id, uid, req_uid, "AUDIT: deny por uid mismatch"); + } + return Decision::Deny; + } + } + if let Some(req_pid) = rule.require_pid { + if pid != req_pid { + if rule.audit { + info!(action_id, pid, req_pid, "AUDIT: deny por pid mismatch"); + } + return Decision::Deny; + } + } + if let Some(d) = rule.decision { + if rule.audit { + info!(action_id, ?d, "AUDIT: rule match con decisión explícita"); + } + return d; + } + // Rule matched pero sin decisión explícita (sólo require_*) y todos + // los requires pasaron — caemos al default. + if rule.audit { + info!(action_id, ?policy.default, "AUDIT: rule match → default"); + } + return policy.default; + } + policy.default +} + +/// Glob simple: `*` matchea cualquier cosa. Soporta prefix (`foo.*`), +/// suffix (`*.bar`) y wildcard exacto (`*`). No es PCRE — intencional. +fn glob_match(pattern: &str, target: &str) -> bool { + if pattern == "*" { return true; } + if let Some(prefix) = pattern.strip_suffix(".*") { + return target == prefix || target.starts_with(&format!("{prefix}.")); + } + if let Some(suffix) = pattern.strip_prefix("*.") { + return target == suffix || target.ends_with(&format!(".{suffix}")); + } + pattern == target +} + +fn load_policy() -> PolicyConfig { + let path = std::env::var("ENTE_POLICY_FILE") + .unwrap_or_else(|_| "/etc/ente/policy.json".into()); + match std::fs::read_to_string(&path) { + Ok(content) => match serde_json::from_str(&content) { + Ok(p) => { info!(path, "policy file cargado"); p } + Err(e) => { + warn!(?e, path, "policy file inválido, usando defaults"); + PolicyConfig::default() + } + }, + Err(_) => { + info!(path, "policy file ausente — usando defaults conservadores"); + PolicyConfig::default() + } + } +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_policy_provider=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-polkit-compat/Cargo.toml b/crates/core/ente-polkit-compat/Cargo.toml new file mode 100644 index 0000000..9aa81f5 --- /dev/null +++ b/crates/core/ente-polkit-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-polkit-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-polkit-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/core/ente-polkit-compat/src/main.rs b/crates/core/ente-polkit-compat/src/main.rs new file mode 100644 index 0000000..53b411c --- /dev/null +++ b/crates/core/ente-polkit-compat/src/main.rs @@ -0,0 +1,262 @@ +//! ente-polkit-compat: shim de `org.freedesktop.PolicyKit1.Authority`. +//! +//! Polkit autoriza llamadas privilegiadas (e.g. SetHostname, PowerOff). +//! En el fractal no usamos polkit como gatekeeper — la auth se hace en +//! el bus interno via SO_PEERCRED y capability grants. Pero apps que +//! usan polkit (gnome-control-center, etc) bloquean en `CheckAuthorization` +//! si no responde nadie. +//! +//! Este shim responde "is_authorized=true" siempre — el fractal queda +//! como sistema confiado. El logging deja audit trail de qué acciones se +//! han pedido para futuro análisis. +//! +//! Producción real: integrar con el grant system del bus interno — +//! CheckAuthorization solicita un token al graph y devuelve true/false +//! según el resultado. + +use ente_bus::{BusClient, BusRequest, BusResponse, POLKIT_DECISION_IFACE, POLKIT_SERVICE_IFACE}; +use ente_card::Capability; +use std::collections::HashMap; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{debug, info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface, zvariant::OwnedValue}; + +const BUS_NAME: &str = "org.freedesktop.PolicyKit1"; +const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/Authority"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-polkit-compat: arrancando"); + announce_to_fractal().await; + + let manager = PolkitAuthority; + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { warn!(?e, "build conn falló — modo idle"); wait_for_term().await } + }, + Err(e) => { warn!(?e, "builder D-Bus falló — modo idle"); wait_for_term().await } + } +} + +struct PolkitAuthority; + +/// Wire format de Polkit: `Subject = (s, a{sv})` — kind ("unix-session", +/// "unix-process", "system-bus-name") + detalles. El detail típico: +/// {"pid": u32, "start-time": u64, "uid": u32} +type Subject = (String, HashMap); + +/// Resultado de `CheckAuthorization`: `(b, b, a{ss})` — +/// is_authorized, is_challenge, details. +type AuthResult = (bool, bool, HashMap); + +#[interface(name = "org.freedesktop.PolicyKit1.Authority")] +impl PolkitAuthority { + async fn check_authorization( + &self, + subject: Subject, + action_id: String, + _details: HashMap, + _flags: u32, + _cancellation_id: String, + ) -> fdo::Result { + let (subj_kind, subj_details) = subject; + let pid = subj_details.get("pid") + .and_then(|v| u32::try_from(v).ok()); + let uid = subj_details.get("uid") + .and_then(|v| u32::try_from(v).ok()); + + // Pregunta al bus interno del fractal si hay un policy provider. + // Si lo hay, su decisión gobierna. Si no (NoProvider), default = allow. + let decision = query_policy(&action_id, pid, uid).await; + info!(%action_id, %subj_kind, ?pid, ?uid, ?decision, "CheckAuthorization"); + Ok((decision.allow, false, HashMap::new())) + } + + async fn check_authorization_by_async( + &self, + subject: Subject, + action_id: String, + details: HashMap, + flags: u32, + cancellation_id: String, + ) -> fdo::Result { + // Mismo comportamiento; algunos clientes llaman la versión async. + self.check_authorization(subject, action_id, details, flags, cancellation_id).await + } + + async fn cancel_check_authorization(&self, _cancellation_id: String) -> fdo::Result<()> { + Ok(()) + } + + async fn enumerate_actions(&self, _locale: String) -> fdo::Result> { + // Devolvemos lista vacía — no enumeramos acciones registradas. + // El llamador (típicamente gnome-control-center settings panel) + // debería degradar grácilmente. + Ok(vec![]) + } + + async fn register_authentication_agent( + &self, + _subject: Subject, + _locale: String, + _object_path: String, + ) -> fdo::Result<()> { + info!("RegisterAuthenticationAgent (no-op)"); + Ok(()) + } + + async fn register_authentication_agent_with_options( + &self, + _subject: Subject, + _locale: String, + _object_path: String, + _options: HashMap, + ) -> fdo::Result<()> { + Ok(()) + } + + async fn unregister_authentication_agent( + &self, + _subject: Subject, + _object_path: String, + ) -> fdo::Result<()> { + Ok(()) + } + + async fn authentication_agent_response( + &self, + _cookie: String, + _identity: (String, HashMap), + ) -> fdo::Result<()> { + Ok(()) + } + + async fn enumerate_temporary_authorizations( + &self, + _subject: Subject, + ) -> fdo::Result> { + Ok(vec![]) + } + + async fn revoke_temporary_authorizations(&self, _subject: Subject) -> fdo::Result<()> { + Ok(()) + } + + async fn revoke_temporary_authorization_by_id(&self, _id: String) -> fdo::Result<()> { + Ok(()) + } + + #[zbus(property)] + async fn backend_name(&self) -> String { "ente-polkit-compat".into() } + + #[zbus(property)] + async fn backend_version(&self) -> String { env!("CARGO_PKG_VERSION").into() } + + #[zbus(property)] + async fn backend_features(&self) -> u32 { 0 } +} + +/// Wire signature de EnumerateActions item: +/// `(ssssssuusa{ss})` — action_id, descripción, message, vendor, vendor_url, +/// icon_name, implicit_any, implicit_inactive, implicit_active, annotations. +type EnumeratedAction = ( + String, String, String, String, String, String, + u32, u32, String, HashMap, +); + +/// Wire signature de TemporaryAuthorization: +/// `(sssss)` — id, action_id, subject_kind, subject_detail, time_obtained, time_expires. +/// Aquí `(string)` * 5 + 2 timestamps. Simplificamos al subset relevante. +type TemporaryAuth = (String, String, (String, HashMap), u64, u64); + +/// Resultado de una consulta de policy al fractal. +#[derive(Debug)] +struct PolicyDecision { + allow: bool, + /// Origen: "fractal" si vino del bus, "default-allow" si no había proveedor. + source: &'static str, +} + +/// Pregunta al bus interno: ¿hay alguien que decida sobre `action_id`? +/// Wire format del blob: `pid_u32_be | uid_u32_be | action_id_utf8`. +/// El proveedor responde con `Invoked { result: [0|1] }` — 1 = allow. +async fn query_policy(action_id: &str, pid: Option, uid: Option) -> PolicyDecision { + let mut blob = Vec::with_capacity(8 + action_id.len()); + blob.extend_from_slice(&pid.unwrap_or(0).to_be_bytes()); + blob.extend_from_slice(&uid.unwrap_or(0).to_be_bytes()); + blob.extend_from_slice(action_id.as_bytes()); + + let mut client = match BusClient::from_env().await { + Ok(c) => c, + Err(e) => { + debug!(?e, "no bus client — default allow"); + return PolicyDecision { allow: true, source: "no-bus" }; + } + }; + let req = BusRequest::Invoke { + cap: Capability::Endpoint { + interface: POLKIT_DECISION_IFACE, + version: 1, + }, + blob, + }; + match client.call(req).await { + Ok(BusResponse::Invoked { result }) => { + let allow = result.first().copied().unwrap_or(1) != 0; + PolicyDecision { allow, source: "fractal" } + } + Ok(BusResponse::Error(msg)) if msg.contains("sin proveedor") => { + // No hay policy provider — default allow. + PolicyDecision { allow: true, source: "default-allow" } + } + Ok(other) => { + warn!(?other, "policy: respuesta inesperada — default allow"); + PolicyDecision { allow: true, source: "default-allow" } + } + Err(e) => { + warn!(?e, "policy: bus call falló — default allow"); + PolicyDecision { allow: true, source: "default-allow" } + } + } +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Endpoint { + interface: POLKIT_SERVICE_IFACE, + version: 1, + }], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_polkit_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-resolved-compat/Cargo.toml b/crates/core/ente-resolved-compat/Cargo.toml new file mode 100644 index 0000000..4a351de --- /dev/null +++ b/crates/core/ente-resolved-compat/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ente-resolved-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-resolved-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +libc = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/core/ente-resolved-compat/src/main.rs b/crates/core/ente-resolved-compat/src/main.rs new file mode 100644 index 0000000..a103bbe --- /dev/null +++ b/crates/core/ente-resolved-compat/src/main.rs @@ -0,0 +1,210 @@ +//! ente-resolved-compat: shim de `org.freedesktop.resolve1`. +//! +//! Bajo el capó usa `tokio::net::lookup_host` (que termina en getaddrinfo +//! del libc del sistema). No reimplementamos un resolver DNS — delegamos +//! al stack de resolución del kernel/glibc. +//! +//! Métodos cubiertos: +//! - ResolveHostname (name → addresses) +//! - ResolveAddress (address → name reverse) +//! - ResolveRecord (TXT/SRV/etc) — NotSupported (requiere DNS query directa) + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::net::IpAddr; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface}; + +const BUS_NAME: &str = "org.freedesktop.resolve1"; +const OBJ_PATH: &str = "/org/freedesktop/resolve1"; + +const AF_INET: i32 = 2; +const AF_INET6: i32 = 10; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-resolved-compat: arrancando"); + announce_to_fractal().await; + + let manager = ResolveManager; + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { warn!(?e, "build conn falló — modo idle"); wait_for_term().await } + }, + Err(e) => { warn!(?e, "builder D-Bus falló — modo idle"); wait_for_term().await } + } +} + +struct ResolveManager; + +/// Tipo del wire format de `ResolveHostname`. Por entry: (ifindex, family, +/// address-as-bytes). systemd-resolved devuelve hasta 4 bytes para AF_INET +/// y 16 para AF_INET6. +type HostnameAddress = (i32, i32, Vec); + +#[interface(name = "org.freedesktop.resolve1.Manager")] +impl ResolveManager { + /// Wire signature: `ResolveHostname(in iiusst, out a(iiay)st)` — recibe + /// (ifindex, name, family, flags), devuelve (addresses, canonical, flags). + async fn resolve_hostname( + &self, + _ifindex: i32, + name: String, + family: i32, + _flags: u64, + ) -> fdo::Result<(Vec, String, u64)> { + // tokio::net::lookup_host requiere "host:port"; usamos puerto sentinel. + let target = format!("{name}:0"); + let addrs = match tokio::net::lookup_host(&target).await { + Ok(it) => it, + Err(e) => return Err(fdo::Error::Failed(format!("lookup_host {name}: {e}"))), + }; + let mut out = Vec::new(); + for sa in addrs { + let ip = sa.ip(); + let (af, bytes) = match ip { + IpAddr::V4(v4) => (AF_INET, v4.octets().to_vec()), + IpAddr::V6(v6) => (AF_INET6, v6.octets().to_vec()), + }; + // Filtrado por family si el llamador lo pidió específico. + if family != 0 && family != af { continue; } + out.push((0i32, af, bytes)); + } + if out.is_empty() { + return Err(fdo::Error::Failed(format!("sin resoluciones para {name} (family={family})"))); + } + info!(%name, family, count = out.len(), "ResolveHostname"); + Ok((out, name, 0)) + } + + /// Wire signature: `ResolveAddress(in iiayt, out a(is)t)` — (ifindex, + /// family, address, flags) → (names, flags). + async fn resolve_address( + &self, + _ifindex: i32, + family: i32, + address: Vec, + _flags: u64, + ) -> fdo::Result<(Vec<(i32, String)>, u64)> { + let ip = parse_address(family, &address) + .ok_or_else(|| fdo::Error::InvalidArgs(format!("address malformado family={family} bytes={}", address.len())))?; + // Reverse lookup vía getnameinfo. Usamos std::net::lookup_addr no existe, + // así que invocamos via libc directamente. + let name = reverse_lookup(ip) + .ok_or_else(|| fdo::Error::Failed(format!("sin reverse para {ip}")))?; + info!(%ip, %name, "ResolveAddress"); + Ok((vec![(0, name)], 0)) + } + + async fn resolve_record( + &self, + _ifindex: i32, + _name: String, + _class: u16, + _type_: u16, + _flags: u64, + ) -> fdo::Result<(Vec<(i32, u16, u16, Vec)>, u64)> { + Err(fdo::Error::NotSupported( + "ResolveRecord requiere acceso DNS directo — stub no implementado".into() + )) + } +} + +fn parse_address(family: i32, bytes: &[u8]) -> Option { + match family { + AF_INET if bytes.len() == 4 => { + let mut a = [0u8; 4]; + a.copy_from_slice(bytes); + Some(IpAddr::V4(std::net::Ipv4Addr::from(a))) + } + AF_INET6 if bytes.len() == 16 => { + let mut a = [0u8; 16]; + a.copy_from_slice(bytes); + Some(IpAddr::V6(std::net::Ipv6Addr::from(a))) + } + _ => None, + } +} + +/// getnameinfo(3) wrapper. Devuelve None si no resuelve. +fn reverse_lookup(ip: IpAddr) -> Option { + use std::os::raw::c_char; + let mut buf = [0i8; 256]; + let r = match ip { + IpAddr::V4(v4) => unsafe { + let octets = v4.octets(); + let mut sin = std::mem::zeroed::(); + sin.sin_family = libc::AF_INET as u16; + sin.sin_addr = libc::in_addr { + s_addr: u32::from_ne_bytes(octets), + }; + libc::getnameinfo( + &sin as *const _ as *const libc::sockaddr, + std::mem::size_of::() as u32, + buf.as_mut_ptr() as *mut c_char, buf.len() as u32, + std::ptr::null_mut(), 0, + libc::NI_NAMEREQD, + ) + }, + IpAddr::V6(v6) => unsafe { + let octets = v6.octets(); + let mut sin6 = std::mem::zeroed::(); + sin6.sin6_family = libc::AF_INET6 as u16; + sin6.sin6_addr.s6_addr.copy_from_slice(&octets); + libc::getnameinfo( + &sin6 as *const _ as *const libc::sockaddr, + std::mem::size_of::() as u32, + buf.as_mut_ptr() as *mut c_char, buf.len() as u32, + std::ptr::null_mut(), 0, + libc::NI_NAMEREQD, + ) + }, + }; + if r != 0 { return None; } + let cs = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }; + cs.to_str().ok().map(String::from) +} + +extern crate libc; + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Endpoint { + interface: ente_card::InterfaceId([0xa3; 16]), + version: 1, + }], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_resolved_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/core/ente-snapshot/Cargo.toml b/crates/core/ente-snapshot/Cargo.toml new file mode 100644 index 0000000..19d36b0 --- /dev/null +++ b/crates/core/ente-snapshot/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ente-snapshot" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +ente-card = { path = "../ente-card" } +serde = { workspace = true } +serde_json = { workspace = true } +ulid = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/core/ente-snapshot/src/lib.rs b/crates/core/ente-snapshot/src/lib.rs new file mode 100644 index 0000000..6eb0d1c --- /dev/null +++ b/crates/core/ente-snapshot/src/lib.rs @@ -0,0 +1,61 @@ +//! Persistencia del fractal. Captura el estado live (Cards encarnadas con +//! sus identidades preservadas) a un blob JSON. Al restaurar, las mismas +//! Ulids vuelven a la vida — los PIDs cambian (kernel no los preserva) pero +//! el grafo se reconstruye con la misma topología. +//! +//! Lo que NO se persiste: +//! - PIDs (irrelevantes tras reboot) +//! - bus_connections (runtime-only) +//! - pending_invokes (en vuelo, se descartan) +//! - device presence (uevents reconstruyen el índice) + +use ente_card::EntityCard; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use ulid::Ulid; + +pub const SNAPSHOT_VERSION: u16 = 1; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FractalSnapshot { + pub version: u16, + pub timestamp_ms: u64, + pub seed_id: Ulid, + pub seed_label: String, + /// Cards live al momento del checkpoint, excluyendo la Semilla. + /// Al restaurar se inyectan en `genesis` con sus Ulids originales. + pub entes: Vec, +} + +impl FractalSnapshot { + pub fn write(&self, path: &Path) -> anyhow::Result<()> { + let bytes = serde_json::to_vec_pretty(self)?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).ok(); + } + // Escritura atómica: temp file + rename. + let tmp = path.with_extension("tmp"); + std::fs::write(&tmp, &bytes)?; + std::fs::rename(&tmp, path)?; + Ok(()) + } + + pub fn read(path: &Path) -> anyhow::Result { + let bytes = std::fs::read(path)?; + let snap: FractalSnapshot = serde_json::from_slice(&bytes)?; + if snap.version != SNAPSHOT_VERSION { + anyhow::bail!( + "snapshot version {} no soportada (esperada {})", + snap.version, SNAPSHOT_VERSION + ); + } + Ok(snap) + } +} + +pub fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} diff --git a/crates/core/ente-soma/Cargo.toml b/crates/core/ente-soma/Cargo.toml new file mode 100644 index 0000000..63df673 --- /dev/null +++ b/crates/core/ente-soma/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ente-soma" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +nix = { workspace = true } +libc = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/core/ente-soma/src/lib.rs b/crates/core/ente-soma/src/lib.rs new file mode 100644 index 0000000..e64ca01 --- /dev/null +++ b/crates/core/ente-soma/src/lib.rs @@ -0,0 +1,362 @@ +//! Encarnación del Soma: traducción de SomaSpec a syscalls. +//! +//! Esta capa es la única parte de PID 1 que toca syscalls de namespacing — +//! todo lo demás opera sobre tipos de alto nivel. La complejidad vive aquí +//! por diseño: encapsulada, auditable, y con un único punto de entrada. +//! +//! ## Protocolo padre↔hijo en el path namespaced +//! +//! ```text +//! parent child +//! | | +//! |--- clone() ------->| (child empieza dentro de los nuevos NS) +//! | | +//! | |---- read(sync_r, 1) ---- (bloquea) +//! | | +//! | write uid_map | +//! | write gid_map | +//! | cgroup move | +//! | cpu affinity | +//! | | +//! |--- write(sync_w) ->| +//! | |---- setrlimit +//! | |---- mount(/, MS_PRIVATE | MS_REC) +//! | |---- execve() +//! ``` + +use ente_card::{CgroupSpec, EntityCard, NamespaceSet, Payload, ResourceLimits}; +use nix::fcntl::OFlag; +use nix::sched::CloneFlags; +use nix::unistd::{pipe2, Pid}; +use std::ffi::CString; +use std::os::fd::{AsRawFd, IntoRawFd, RawFd}; +use std::process::Command; +use std::sync::OnceLock; +use tracing::{info, warn}; + +/// Path del socket del bus interno. Se establece una sola vez al arrancar +/// PID 1 (después de que el listener bind exitoso). Cada hijo encarnado +/// recibe este path en `ENTE_BUS_SOCK`. +static BUS_SOCK_PATH: OnceLock = OnceLock::new(); + +pub fn set_bus_sock(path: String) { + let _ = BUS_SOCK_PATH.set(path); +} + +fn build_env(card: &EntityCard, base_envp: &[(String, String)]) -> Vec<(String, String)> { + // Heredamos parent env, sobreescribimos con el envp explícito de la Card, + // y al final inyectamos las vars del fractal (no negociables). + let mut env: Vec<(String, String)> = std::env::vars().collect(); + for (k, v) in base_envp { + env.retain(|(ek, _)| ek != k); + env.push((k.clone(), v.clone())); + } + if let Some(p) = BUS_SOCK_PATH.get() { + env.retain(|(k, _)| k != ente_bus::ENV_BUS_SOCK); + env.push((ente_bus::ENV_BUS_SOCK.into(), p.clone())); + } + env.retain(|(k, _)| k != ente_bus::ENV_ENTE_ID); + env.push((ente_bus::ENV_ENTE_ID.into(), card.id.to_string())); + // Apps `Type=notify` (sd_notify) leen NOTIFY_SOCKET. Apuntamos al path + // canónico de systemd; si ente-notify-compat no está corriendo, apps + // sólo verán que sd_notify falla y siguen sin "ready" signal — no es fatal. + env.retain(|(k, _)| k != "NOTIFY_SOCKET"); + env.push(("NOTIFY_SOCKET".into(), "/run/systemd/notify".into())); + env +} + +pub fn incarnate(card: &EntityCard) -> anyhow::Result { + if needs_namespacing(&card.soma.namespaces) { + incarnate_namespaced(card) + } else { + incarnate_plain(card) + } +} + +fn needs_namespacing(ns: &NamespaceSet) -> bool { + ns.mount || ns.pid || ns.net || ns.uts || ns.ipc || ns.user || ns.cgroup +} + +/// Path simple: para Entes que no requieren aislamiento. Útil para Entes-shim +/// que conviven con el host (e.g. compat-logind) y para dev mode. +fn incarnate_plain(card: &EntityCard) -> anyhow::Result { + let (exec, argv, base_envp) = match &card.payload { + Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()), + Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()), + _ => anyhow::bail!("incarnate_plain: payload no ejecutable"), + }; + let env = build_env(card, &base_envp); + let mut cmd = Command::new(&exec); + cmd.args(&argv); + cmd.env_clear(); + for (k, v) in &env { + cmd.env(k, v); + } + let child = cmd.spawn().map_err(|e| anyhow::anyhow!("spawn {exec}: {e}"))?; + Ok(Pid::from_raw(child.id() as i32)) +} + +/// Path namespaced: clone(2) + sync pipe + setup post-clone en padre + finalize en hijo. +fn incarnate_namespaced(card: &EntityCard) -> anyhow::Result { + let flags = build_clone_flags(&card.soma.namespaces); + info!(label = %card.label, ?flags, "namespaced incarnation"); + + let (exec, argv, base_envp) = match &card.payload { + Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()), + Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()), + _ => anyhow::bail!("incarnate_namespaced: payload no ejecutable"), + }; + + // Pipe O_CLOEXEC: el read del lado hijo es lo que hace race-free el setup. + // O_CLOEXEC garantiza que el fd se cierra automáticamente en execve, así + // no contamina el binario final. + let (sync_r, sync_w) = pipe2(OFlag::O_CLOEXEC)?; + let sync_r_raw: RawFd = sync_r.into_raw_fd(); + let sync_w_raw: RawFd = sync_w.into_raw_fd(); + + let exec_c = CString::new(exec.clone())?; + let argv_c: Vec = std::iter::once(exec_c.clone()) + .chain(argv.iter().filter_map(|s| CString::new(s.as_str()).ok())) + .collect(); + let argv_ptrs: Vec<*const libc::c_char> = argv_c.iter() + .map(|c| c.as_ptr()) + .chain(std::iter::once(std::ptr::null())) + .collect(); + + // envp construido pre-clone: padre y hijo comparten el COW. Tras execve + // el kernel reemplaza el address space, así que las CStrings sólo viven + // hasta el syscall. + let env_pairs = build_env(card, &base_envp); + let envp_c: Vec = env_pairs.iter() + .filter_map(|(k, v)| CString::new(format!("{k}={v}")).ok()) + .collect(); + let envp_ptrs: Vec<*const libc::c_char> = envp_c.iter() + .map(|c| c.as_ptr()) + .chain(std::iter::once(std::ptr::null())) + .collect(); + + let rlimits = card.soma.rlimits.clone(); + let mount_ns_enabled = card.soma.namespaces.mount; + + // SAFETY: la clausura corre en stack nuevo dentro de un proceso recién + // clonado, COW del padre. Reglas inviolables: + // - sólo syscalls async-signal-safe + // - no `println!`/`tracing!`/cualquier I/O del runtime + // - no allocator (vec/box/string) + // - no Drop con efectos + // - capturar sólo Copy o datos pre-construidos + let cb = Box::new(move || -> isize { + // 1) Cerrar el extremo de escritura: pertenece al padre. + unsafe { libc::close(sync_w_raw); } + + // 2) Bloquear hasta que el padre termine el setup (uid_map, cgroup, etc). + let mut byte = [0u8; 1]; + let n = unsafe { + libc::read(sync_r_raw, byte.as_mut_ptr() as *mut _, 1) + }; + if n != 1 { unsafe { libc::_exit(101); } } + unsafe { libc::close(sync_r_raw); } + + // 3) Aplicar rlimits dentro del nuevo namespace. + unsafe { apply_rlimits_unchecked(&rlimits); } + + // 4) Si tenemos mount ns, marcar / como privado recursivamente para + // que mounts del Ente no se filtren al host (es la trampa más + // típica al delegar mount ns). + if mount_ns_enabled { + unsafe { + libc::mount( + std::ptr::null(), + b"/\0".as_ptr() as *const _, + std::ptr::null(), + libc::MS_PRIVATE | libc::MS_REC, + std::ptr::null(), + ); + } + } + + // 5) execve. Si retorna, falló. + unsafe { + libc::execve(exec_c.as_ptr(), argv_ptrs.as_ptr(), envp_ptrs.as_ptr()); + libc::_exit(102); + } + }); + + let mut stack = vec![0u8; 1024 * 1024]; + + #[allow(deprecated)] + let pid = unsafe { + nix::sched::clone(cb, &mut stack, flags, Some(libc::SIGCHLD)) + }.map_err(|e| { + unsafe { libc::close(sync_r_raw); libc::close(sync_w_raw); } + anyhow::anyhow!("clone failed: {e}") + })?; + + // Padre: cerrar el extremo de lectura. + unsafe { libc::close(sync_r_raw); } + + // Setup post-clone en padre. Errores aquí no son fatales — registramos y + // continuamos. Si algo crítico falla, el hijo execve seguirá adelante con + // configuración degradada y el supervisor decidirá qué hacer. + if let Err(e) = configure_child(pid, card) { + warn!(?e, ?pid, "configure_child errores no-fatales"); + } + + // Despertar al hijo. + let signal_byte = [b'x']; + let written = unsafe { + libc::write(sync_w_raw, signal_byte.as_ptr() as *const _, 1) + }; + unsafe { libc::close(sync_w_raw); } + if written != 1 { + warn!(?pid, "no se pudo señalizar al hijo (write devolvió {})", written); + } + + if matches!(&card.payload, Payload::Legacy { fakes, .. } if !fakes.is_empty()) { + // TODO: facades viven en un Ente-shim aparte que se inyecta vía + // bind-mount sobre /run/systemd/notify, /run/dbus/system_bus_socket, + // etc. Cuando exista, registrarlas aquí. + warn!("legacy facades declaradas pero shim post-clone no implementado"); + } + + Ok(pid) +} + +/// Setup que requiere capacidades del padre: uid_map, gid_map, cgroup move. +/// Estos archivos en /proc//* tienen reglas de propiedad que sólo el +/// padre puede satisfacer mientras el hijo está suspendido en el sync pipe. +fn configure_child(pid: Pid, card: &EntityCard) -> anyhow::Result<()> { + if card.soma.namespaces.user { + // Desde kernel 3.19 se debe escribir "deny" a setgroups antes de + // poder escribir gid_map sin CAP_SETGID. Ignorar errores: en kernels + // antiguos el archivo no existe y no es problema. + let _ = std::fs::write(format!("/proc/{}/setgroups", pid.as_raw()), "deny"); + + let uid = nix::unistd::getuid().as_raw(); + let gid = nix::unistd::getgid().as_raw(); + std::fs::write( + format!("/proc/{}/uid_map", pid.as_raw()), + format!("0 {uid} 1"), + ).map_err(|e| anyhow::anyhow!("write uid_map: {e}"))?; + std::fs::write( + format!("/proc/{}/gid_map", pid.as_raw()), + format!("0 {gid} 1"), + ).map_err(|e| anyhow::anyhow!("write gid_map: {e}"))?; + } + + if !card.soma.cgroup.path.is_empty() { + match ensure_cgroup(&card.soma.cgroup) { + Ok(abs_path) => { + let procs = format!("{abs_path}/cgroup.procs"); + if let Err(e) = std::fs::write(&procs, format!("{}\n", pid.as_raw())) { + warn!(?e, path = %procs, "cgroup move falló"); + } + } + Err(e) => warn!(?e, path = %card.soma.cgroup.path, "ensure_cgroup falló"), + } + } + + if let Some(cpus) = &card.soma.cpu_affinity { + if let Err(e) = set_cpu_affinity(pid, cpus) { + warn!(?e, ?pid, "sched_setaffinity falló"); + } + } + + Ok(()) +} + +fn set_cpu_affinity(pid: Pid, cpus: &[u32]) -> anyhow::Result<()> { + let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; + unsafe { libc::CPU_ZERO(&mut set); } + for &c in cpus { + unsafe { libc::CPU_SET(c as usize, &mut set); } + } + let r = unsafe { + libc::sched_setaffinity(pid.as_raw(), std::mem::size_of::(), &set) + }; + if r != 0 { + anyhow::bail!("sched_setaffinity: {}", std::io::Error::last_os_error()); + } + Ok(()) +} + +/// SAFETY: invocada en el hijo post-clone, sólo libc, no Rust I/O. +unsafe fn apply_rlimits_unchecked(rl: &ResourceLimits) { + if let Some(mem) = rl.mem_bytes { + let lim = libc::rlimit { rlim_cur: mem, rlim_max: mem }; + libc::setrlimit(libc::RLIMIT_AS, &lim); + } + if let Some(np) = rl.nproc { + let lim = libc::rlimit { rlim_cur: np as u64, rlim_max: np as u64 }; + libc::setrlimit(libc::RLIMIT_NPROC, &lim); + } + if let Some(nf) = rl.nofile { + let lim = libc::rlimit { rlim_cur: nf as u64, rlim_max: nf as u64 }; + libc::setrlimit(libc::RLIMIT_NOFILE, &lim); + } +} + +/// Cgroup actual del proceso PID 1 (o ente-zero en dev). Lo usamos como +/// prefijo para paths declarados relativos en CgroupSpec.path. En prod (PID 1 +/// como child del kernel) será `/`. En dev bajo systemd-user será algo como +/// `/user.slice/user-1001.slice/user@1001.service/...`. +fn current_cgroup() -> Option { + let s = std::fs::read_to_string("/proc/self/cgroup").ok()?; + // Formato unified (cgroup v2): "0::/user.slice/..." + s.lines() + .find_map(|l| l.strip_prefix("0::")) + .map(|s| s.trim().to_string()) +} + +/// Resuelve un path declarado en CgroupSpec contra la jerarquía real. +/// - path absoluto (empieza con `/`): respetar tal cual +/// - path relativo: prefijar con cgroup actual de PID 1 +fn resolve_cgroup_path(spec_path: &str) -> String { + if spec_path.is_empty() { return String::new(); } + if spec_path.starts_with('/') { + return spec_path.to_string(); + } + let trimmed = spec_path.trim_start_matches('/'); + if let Some(cg) = current_cgroup() { + let base = if cg == "/" { String::new() } else { cg.trim_end_matches('/').to_string() }; + format!("{base}/{trimmed}") + } else { + format!("/{trimmed}") + } +} + +/// Crea el cgroup declarado, aplica weights. Devuelve el path absoluto +/// resultante bajo /sys/fs/cgroup. +fn ensure_cgroup(spec: &CgroupSpec) -> anyhow::Result { + let rel = resolve_cgroup_path(&spec.path); + if rel.is_empty() { + anyhow::bail!("cgroup path vacío"); + } + let abs = format!("/sys/fs/cgroup{}", rel); + std::fs::create_dir_all(&abs) + .map_err(|e| anyhow::anyhow!("mkdir {}: {e}", abs))?; + if let Some(w) = spec.cpu_weight { + let _ = std::fs::write(format!("{abs}/cpu.weight"), format!("{w}\n")); + } + if let Some(w) = spec.io_weight { + // io.weight requiere el formato "default " en cgroup v2. + let _ = std::fs::write(format!("{abs}/io.weight"), format!("default {w}\n")); + } + Ok(abs) +} + +fn build_clone_flags(ns: &NamespaceSet) -> CloneFlags { + let mut f = CloneFlags::empty(); + if ns.mount { f |= CloneFlags::CLONE_NEWNS; } + if ns.pid { f |= CloneFlags::CLONE_NEWPID; } + if ns.net { f |= CloneFlags::CLONE_NEWNET; } + if ns.uts { f |= CloneFlags::CLONE_NEWUTS; } + if ns.ipc { f |= CloneFlags::CLONE_NEWIPC; } + if ns.user { f |= CloneFlags::CLONE_NEWUSER; } + if ns.cgroup { f |= CloneFlags::CLONE_NEWCGROUP; } + f +} + +// AsRawFd unused but keep the import alive — soma may grow more fd handling. +#[allow(dead_code)] +fn _keep_imports(_: &dyn AsRawFd) {} diff --git a/crates/core/ente-systemd1-compat/Cargo.toml b/crates/core/ente-systemd1-compat/Cargo.toml new file mode 100644 index 0000000..425521e --- /dev/null +++ b/crates/core/ente-systemd1-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-systemd1-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-systemd1-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/core/ente-systemd1-compat/src/main.rs b/crates/core/ente-systemd1-compat/src/main.rs new file mode 100644 index 0000000..81ae1ba --- /dev/null +++ b/crates/core/ente-systemd1-compat/src/main.rs @@ -0,0 +1,280 @@ +//! ente-systemd1-compat: shim de `org.freedesktop.systemd1.Manager`. +//! +//! Centro de control que `systemctl` consulta. Sin esto, `systemctl list-units` +//! falla con `Failed to connect to bus` aunque el sistema funcione. +//! +//! Mapeo: cada Ente vivo del fractal aparece como una "unit" cuyo nombre es +//! `