diff --git a/Cargo.lock b/Cargo.lock index f91fd3b5a7c..e86620557da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,15 +185,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arc-swap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" -dependencies = [ - "rustversion", -] - [[package]] name = "argon2" version = "0.5.3" @@ -206,12 +197,6 @@ dependencies = [ "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.8" @@ -275,12 +260,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "base64" version = "0.23.1" @@ -320,12 +299,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" version = "1.3.2" @@ -429,19 +402,6 @@ dependencies = [ "toyos-tsc", "toyos-update", "toyos-wallclock", - "uefi", - "uefi-services", -] - -[[package]] -name = "bstr" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" -dependencies = [ - "memchr", - "regex-automata", - "serde_core", ] [[package]] @@ -462,12 +422,6 @@ 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 = "byteorder_slice" version = "3.0.0" @@ -623,15 +577,6 @@ dependencies = [ "inout 0.2.2", ] -[[package]] -name = "clru" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "cmov" version = "0.5.3" @@ -736,12 +681,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core_detect" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" - [[package]] name = "coreaudio-rs" version = "0.14.2" @@ -835,15 +774,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.23" @@ -976,20 +906,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dashmap" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -1002,52 +918,6 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "defmt" -version = "0.3.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" -dependencies = [ - "defmt 1.1.1", -] - -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.18", -] - [[package]] name = "delegate" version = "0.13.5" @@ -1208,12 +1078,6 @@ name = "dpi" version = "0.1.1" source = "git+https://github.com/ToyOSOrg/winit?branch=toyos-0.30.13#f58e1f3bca784d97056fe47475c2dca8c6386816" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "ecdsa" version = "0.17.0-rc.16" @@ -1308,20 +1172,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "encoding_rs" -version = "0.8.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" -dependencies = [ - "cfg-if", - "core_detect", - "multiversion_no_op", - "rustversion", - "scopeguard", - "simdutf8", -] - [[package]] name = "enum_dispatch" version = "0.3.13" @@ -1359,27 +1209,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "euclid" -version = "0.22.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" -dependencies = [ - "num-traits", -] - -[[package]] -name = "faster-hex" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed" -dependencies = [ - "autocfg", - "defmt 0.3.100", - "heapless", - "serde", -] - [[package]] name = "fastrand" version = "2.5.0" @@ -1398,15 +1227,6 @@ dependencies = [ "log", ] -[[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" @@ -1482,34 +1302,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide 0.9.1", + "miniz_oxide", "zlib-rs", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[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 = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "fontdue" version = "0.9.3" @@ -1730,1047 +1532,177 @@ dependencies = [ ] [[package]] -name = "gix" -version = "0.88.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d6dd6028308ad2f2662207506e396b97f9ec899f74bd537c0775248c774f21" -dependencies = [ - "gix-actor", - "gix-attributes", - "gix-command", - "gix-commitgraph", - "gix-config", - "gix-date", - "gix-diff", - "gix-dir", - "gix-discover", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-hashtable", - "gix-ignore", - "gix-index", - "gix-lock", - "gix-note", - "gix-object", - "gix-odb", - "gix-pack", - "gix-path", - "gix-pathspec", - "gix-protocol", - "gix-quote", - "gix-ref", - "gix-refspec", - "gix-revision", - "gix-revwalk", - "gix-sec", - "gix-shallow", - "gix-status", - "gix-submodule", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-url", - "gix-utils", - "gix-validate", - "gix-worktree", - "gix-worktree-stream", - "gix-zlib", - "nonempty", - "smallvec", -] - -[[package]] -name = "gix-actor" -version = "0.43.0" +name = "gpt" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e046e9a929e8e1f40f9a34f736408c4c079f9cf004429f3e718d270e847be96" +checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" dependencies = [ - "bstr", - "gix-date", - "gix-error", + "bitflags 2.11.0", + "crc", + "log", + "uuid", ] [[package]] -name = "gix-attributes" -version = "0.36.0" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d57bcf18768dc259868e335fae99a9063174178b78b00348603f0791cd780a" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "bstr", - "gix-error", - "gix-features", - "gix-glob", - "gix-path", - "gix-quote", - "gix-trace", - "smallvec", - "unicode-bom", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] -name = "gix-bitmap" -version = "0.5.0" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9d28a8333a322df5c4fdc7b86a9928965e50794523c0edff57d1fbf2a520cf" -dependencies = [ - "gix-error", -] +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "gix-chunk" -version = "0.9.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25303d07ce2b8f98aab48ce4251317d785a3208b7ed65698928e4a2f31889936" -dependencies = [ - "gix-error", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "gix-command" -version = "0.11.0" +name = "hermit-abi" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3c2e61f97adc08764060e9becb0c819d8f2ba27896434710e039ae57b1d2a7" -dependencies = [ - "bstr", - "gix-error", - "gix-path", - "gix-quote", - "gix-trace", -] +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] -name = "gix-commitgraph" -version = "0.40.0" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0462cbffd5828447f376559d397d9b29f1979dac3d980b5212b17fa174b174eb" -dependencies = [ - "bstr", - "gix-chunk", - "gix-error", - "gix-hash", - "memmap2", - "nonempty", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "gix-config" -version = "0.61.0" +name = "hex-literal" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2cb454dce2f1895cf9def1df8b0a6bbc579d86d8313bdf60f1ab429d52df12" -dependencies = [ - "bstr", - "gix-config-value", - "gix-error", - "gix-features", - "gix-glob", - "gix-path", - "gix-ref", - "gix-sec", - "gix-utils", - "smallvec", - "unicode-bom", -] +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] -name = "gix-config-value" -version = "0.20.0" +name = "hkdf" +version = "0.13.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1894e816c774650b5157d965853b50d7e8f7137ea74055a6bac2f2ec051cf997" +checksum = "cbb55385998ae66b8d2d5143c05c94b9025ab863966f0c94ce7a5fde30105092" dependencies = [ - "bitflags 2.11.0", - "bstr", - "gix-error", - "gix-path", - "libc", + "hmac 0.13.0-rc.5", ] [[package]] -name = "gix-date" -version = "0.17.0" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8beceb16a77fb222923592864b94beb4ad4c2b175a7055c2cac6815ee0370a" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "bstr", - "gix-error", - "itoa", - "jiff", + "digest 0.10.7", ] [[package]] -name = "gix-diff" -version = "0.68.0" +name = "hmac" +version = "0.13.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "939aee7bbc9f233de858839e865c82886e0baceb8f1fe792ea1703fa895f09e1" +checksum = "ef451d73f36d8a3f93ad32c332ea01146c9650e1ec821a9b0e46c01277d544f8" dependencies = [ - "bstr", - "gix-attributes", - "gix-command", - "gix-error", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-imara-diff", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-worktree", + "digest 0.11.1", ] [[package]] -name = "gix-dir" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e41d125c2c6f981aad27299a6233a7ace3413289c1e2994f57e5bab1c34847" -dependencies = [ - "bstr", - "gix-discover", - "gix-error", - "gix-fs", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-trace", - "gix-utils", - "gix-worktree", -] +name = "host" +version = "0.1.0" [[package]] -name = "gix-discover" -version = "0.56.0" +name = "http" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4476ba055ec5fbb05f59dd98380b596428da060beac5f9dccd7374302ad1fbfb" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ - "bstr", - "dunce", - "gix-error", - "gix-fs", - "gix-path", - "gix-ref", - "gix-sec", + "bytes", + "itoa", ] [[package]] -name = "gix-error" -version = "0.4.0" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a5c4cd326ddfe7f046ad9eed05461c59af6ea98b681648bac73954317addd" -dependencies = [ - "bstr", -] +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "gix-features" -version = "0.50.0" +name = "hybrid-array" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb3a430dbc02d6c5e30a9f92083459593495ea22a476e837d248b22d800eed80" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" dependencies = [ - "bytes", - "crc32fast", - "crossbeam-channel", - "gix-path", - "gix-trace", - "gix-utils", - "libc", - "once_cell", - "parking_lot", - "prodash", - "walkdir", + "ctutils", + "subtle", + "typenum", + "zeroize", ] [[package]] -name = "gix-filter" -version = "0.35.0" +name = "iana-time-zone" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f31a39e2c9b04843709d1f72b2c39c98dc0ee591e2f67d21302dbfbd06d2956" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ - "bstr", - "encoding_rs", - "gix-attributes", - "gix-command", - "gix-error", - "gix-hash", - "gix-object", - "gix-packetline", - "gix-path", - "gix-quote", - "gix-trace", - "gix-utils", - "smallvec", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", ] [[package]] -name = "gix-fs" -version = "0.23.0" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0105ab1cef4542d092c27766d2daf1548fcbad7e460dc2208642c120bb19b3ad" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "bstr", - "gix-error", - "gix-features", - "gix-path", - "gix-utils", + "cc", ] [[package]] -name = "gix-glob" -version = "0.28.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bba50803dea16646cafb9a2d67e95b8cdf5b21c7e8c45bcd18f38bafe02fb81f" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "gix-features", - "gix-path", - "gix-utils", -] +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "gix-hash" -version = "0.27.0" +name = "indexmap" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a22b6febaaee8546eb0c9dd21ecdd93aca269a6cb3897e1250f0f60933c0174" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ - "faster-hex", - "gix-error", - "gix-features", - "sha1dc", + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] -name = "gix-hashtable" -version = "0.17.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55bff2f32f6f5a95bdd43e5e5e01b0656250cffbc48eda19db0afa3d73adf2f" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "gix-hash", - "hashbrown 0.17.1", - "parking_lot", + "block-padding 0.3.3", + "generic-array 0.14.7", ] [[package]] -name = "gix-ignore" -version = "0.23.0" +name = "inout" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfd910e271ecd1a20e4df4009a0d9d844a0c51ee5444aaa3fadd1b1b077930b" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "bstr", - "gix-glob", - "gix-path", - "gix-trace", - "unicode-bom", -] - -[[package]] -name = "gix-imara-diff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305657ad44356151d73c145f567c25b24fdf2466bfb3e90727e1d5461a4e9748" -dependencies = [ - "bstr", - "hashbrown 0.17.1", -] - -[[package]] -name = "gix-index" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfd5b484b5b4e218a5ac114f0982949f9bd7582ff67986665a0cf85895332433" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "filetime", - "fnv", - "gix-bitmap", - "gix-error", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-traverse", - "gix-utils", - "gix-validate", - "hashbrown 0.17.1", - "itoa", - "libc", - "memmap2", - "rustix 1.1.5", - "smallvec", -] - -[[package]] -name = "gix-lock" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd5ddfd0cc476fef744ac544783b96a9ff5d49f8de541e6e9d6d6002aa90720" -dependencies = [ - "gix-error", - "gix-tempfile", - "gix-utils", -] - -[[package]] -name = "gix-macros" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11f5429ca73cc3758c9f53af56a9fba100a8701c5b4bc30e29b8b0b15288991" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gix-note" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6879cc9e5a9a0429e40b05970af6c8dad8734098a3142fec23e32eeb567580" -dependencies = [ - "gix-error", - "gix-hash", - "gix-object", -] - -[[package]] -name = "gix-object" -version = "0.65.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a72909eeeed845721a143ef3dbeb4d6cd5df79c839fe7eefc138cb4f982eba3e" -dependencies = [ - "bstr", - "gix-actor", - "gix-command", - "gix-date", - "gix-error", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-path", - "gix-tempfile", - "gix-utils", - "gix-validate", - "itoa", - "smallvec", -] - -[[package]] -name = "gix-odb" -version = "0.85.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5794b85ff563a290ea18867fa1f0a5b0b84709f2f34dddbb3614556fbe88c629" -dependencies = [ - "arc-swap", - "gix-error", - "gix-features", - "gix-fs", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-pack", - "gix-path", - "gix-quote", - "gix-zlib", - "memmap2", - "parking_lot", - "tempfile", -] - -[[package]] -name = "gix-pack" -version = "0.75.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0451a48faef8e0e7a8f6d903c8329c7d18a793b3030907ca841f889dc695d1df" -dependencies = [ - "clru", - "gix-chunk", - "gix-error", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-path", - "gix-zlib", - "memmap2", - "smallvec", -] - -[[package]] -name = "gix-packetline" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba540112a3fcc6388eb2aa4dc4837040478c285a35178cd9a44b07345cd6636" -dependencies = [ - "bstr", - "faster-hex", - "gix-error", - "gix-trace", -] - -[[package]] -name = "gix-path" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd051ac60c5dbce2228daabf25b6a76408ac732d416c678cfa0a4914d13f60b8" -dependencies = [ - "bstr", - "gix-error", - "gix-trace", - "gix-validate", -] - -[[package]] -name = "gix-pathspec" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee883ab64bad641bf31ff338044edd7aa010605f19e866d98c9164e635c38b1" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "gix-attributes", - "gix-config-value", - "gix-error", - "gix-glob", - "gix-path", -] - -[[package]] -name = "gix-protocol" -version = "0.66.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331ac489c461790b6b692afb322085c42041c6ff7621c30ac3dbb00b12e05e89" -dependencies = [ - "bstr", - "gix-date", - "gix-error", - "gix-features", - "gix-hash", - "gix-macros", - "gix-ref", - "gix-shallow", - "gix-transport", - "gix-utils", - "nonempty", -] - -[[package]] -name = "gix-quote" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b040cfa7af50f131bfa34119cceee804c3cc40c766daaba8e0c8149340aea7" -dependencies = [ - "bstr", - "gix-error", - "gix-utils", -] - -[[package]] -name = "gix-ref" -version = "0.68.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90ceeec67de03b737c30b1b69116ca56fec803edde022f3d25181f0f597083d" -dependencies = [ - "gix-actor", - "gix-error", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-utils", - "gix-validate", - "memmap2", -] - -[[package]] -name = "gix-refspec" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4caa89a034a055c0578150092d3edb2f1c3f74cca86b883263a28caa26c5812" -dependencies = [ - "bstr", - "gix-error", - "gix-hash", - "gix-validate", - "smallvec", -] - -[[package]] -name = "gix-revision" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8438d38e197fa3ea7bb297302db1701879a64a343f23a3d150a602a609a8751" -dependencies = [ - "bstr", - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-object", - "gix-revwalk", - "nonempty", -] - -[[package]] -name = "gix-revwalk" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa6d913e7cf8c6407d3954f9794b4b148c2113c672c34b63c59ea654a1e060e" -dependencies = [ - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-hashtable", - "gix-object", - "smallvec", -] - -[[package]] -name = "gix-sec" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8eb983a830ba586b61f7005deaf44609e1a4186cbf13a0e8668154ca910461" -dependencies = [ - "bitflags 2.11.0", - "gix-path", - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-shallow" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119bae1301715df40e8fd136deeff4fe8a3be4cfc001ed5e72bad7b47ebbc090" -dependencies = [ - "bstr", - "gix-error", - "gix-hash", - "gix-lock", - "nonempty", -] - -[[package]] -name = "gix-status" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb7e035b6d9dce932f6b6c8d33d29b31b46e05da544bf810e324bf61aaa5b84" -dependencies = [ - "bstr", - "filetime", - "gix-diff", - "gix-dir", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-worktree", - "hashbrown 0.16.1", - "portable-atomic", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-submodule" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b498192e5f89453fbd728263d52088aeec634a390e732f1da6d0a491c552091" -dependencies = [ - "bstr", - "gix-config", - "gix-error", - "gix-path", - "gix-pathspec", - "gix-refspec", - "gix-url", -] - -[[package]] -name = "gix-tempfile" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dd7dd8df3345933caddd6ce67821c9eec17192564f1011cf0bd0b8cb0d07ffb" -dependencies = [ - "dashmap", - "gix-fs", - "libc", - "parking_lot", - "tempfile", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-trace" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df45b2cf6052d9e52681130c17d67505b023e8933ad974f94a2be0b0d24f636a" - -[[package]] -name = "gix-transport" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2beb7b0387bbf4d7c13763c6e168f9957319aa811f09349dc59601be3b2951" -dependencies = [ - "bstr", - "gix-command", - "gix-error", - "gix-features", - "gix-packetline", - "gix-path", - "gix-quote", - "gix-sec", - "gix-url", -] - -[[package]] -name = "gix-traverse" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4864934e4efd92082ecc37107bce0c389b58d82888db634817f9c711efb7ed" -dependencies = [ - "bitflags 2.11.0", - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-revwalk", - "smallvec", -] - -[[package]] -name = "gix-url" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f30fe2a228d7de5f9d582463d9b724debcb870162d9fbe908cd5d76ccdecf0d" -dependencies = [ - "bstr", - "gix-error", - "gix-path", - "gix-utils", - "percent-encoding", -] - -[[package]] -name = "gix-utils" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70525766f53426ca4cd2ea4d58b0b31488dfedb416b3254e2efdd9dc184cc880" -dependencies = [ - "bstr", - "fastrand", - "getrandom 0.4.2", - "unicode-normalization", -] - -[[package]] -name = "gix-validate" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30574f81fc41b2ca3582ece2bdf8696bdfe9c170aa7e8bcafb81199c86b1ceea" -dependencies = [ - "bstr", - "gix-error", -] - -[[package]] -name = "gix-worktree" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04fa19b9b3961aafb3b9e60005b9bcef0100f8b211f46ab5269a6ac896073780" -dependencies = [ - "bstr", - "gix-attributes", - "gix-error", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-validate", -] - -[[package]] -name = "gix-worktree-stream" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2021e1609beae0edbeefa76ac641be0571b725afacb77ac5b12f33b436e0d3d9" -dependencies = [ - "gix-attributes", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-object", - "gix-path", - "gix-traverse", - "parking_lot", -] - -[[package]] -name = "gix-zlib" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44a1a9c82ec5b712082566cbef8f5782472a0d3973132911e5a83380a5298fe2" -dependencies = [ - "gix-error", - "zlib-rs", -] - -[[package]] -name = "gpt" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" -dependencies = [ - "bitflags 2.11.0", - "crc", - "log", - "uuid", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex-literal" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" - -[[package]] -name = "hkdf" -version = "0.13.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbb55385998ae66b8d2d5143c05c94b9025ab863966f0c94ce7a5fde30105092" -dependencies = [ - "hmac 0.13.0-rc.5", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac" -version = "0.13.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef451d73f36d8a3f93ad32c332ea01146c9650e1ec821a9b0e46c01277d544f8" -dependencies = [ - "digest 0.11.1", -] - -[[package]] -name = "host" -version = "0.1.0" - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hybrid-array" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" -dependencies = [ - "ctutils", - "subtle", - "typenum", - "zeroize", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "imagesize" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding 0.3.3", - "generic-array 0.14.7", -] - -[[package]] -name = "inout" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" -dependencies = [ - "block-padding 0.4.2", - "hybrid-array", + "block-padding 0.4.2", + "hybrid-array", ] [[package]] @@ -2829,60 +1761,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jiff" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" -dependencies = [ - "defmt 1.1.1", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" -dependencies = [ - "defmt 1.1.1", - "log", -] - -[[package]] -name = "jiff-static" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - [[package]] name = "jni" version = "0.21.1" @@ -3054,17 +1932,6 @@ dependencies = [ "toyos-abi", ] -[[package]] -name = "kurbo" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3126,15 +1993,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[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" @@ -3224,16 +2082,6 @@ 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 = "miniz_oxide" version = "0.9.1" @@ -3280,22 +2128,6 @@ dependencies = [ "num-traits", ] -[[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 = "multiversion_no_op" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" - [[package]] name = "ndk" version = "0.9.0" @@ -3371,12 +2203,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonempty" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" - [[package]] name = "ntapi" version = "0.4.3" @@ -3421,15 +2247,6 @@ dependencies = [ "smallvec", ] -[[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.2" @@ -3939,29 +2756,6 @@ dependencies = [ "toyos-window", ] -[[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", -] - -[[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", -] - [[package]] name = "password-hash" version = "0.5.0" @@ -4010,7 +2804,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" dependencies = [ - "base64 0.23.1", + "base64", "serde_core", ] @@ -4038,12 +2832,6 @@ 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.11" @@ -4131,19 +2919,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.11.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - [[package]] name = "polling" version = "3.11.0" @@ -4192,21 +2967,6 @@ dependencies = [ "universal-hash 0.6.1", ] -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" -dependencies = [ - "portable-atomic", -] - [[package]] name = "powerfmt" version = "0.2.1" @@ -4232,15 +2992,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - [[package]] name = "primefield" version = "0.14.0-rc.8" @@ -4286,41 +3037,6 @@ dependencies = [ name = "proctest" version = "0.1.0" -[[package]] -name = "prodash" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" -dependencies = [ - "parking_lot", -] - -[[package]] -name = "ptr_meta" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcada80daa06c42ed5f48c9a043865edea5dc44cbf9ac009fda3b89526e28607" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca9224df2e20e7c5548aeb5f110a0f3b77ef05f8585139b7148b59056168ed2" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "pxfm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" - [[package]] name = "quick-xml" version = "0.41.0" @@ -4446,15 +3162,6 @@ dependencies = [ "yasna", ] -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -4499,20 +3206,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "resvg" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81" -dependencies = [ - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - [[package]] name = "rfc6979" version = "0.5.0-rc.5" @@ -4523,15 +3216,6 @@ dependencies = [ "subtle", ] -[[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" @@ -4545,15 +3229,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "roxmltree" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" -dependencies = [ - "memchr", -] - [[package]] name = "rsa" version = "0.10.0-rc.17" @@ -4579,10 +3254,8 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" dependencies = [ - "num-complex", "num-integer", "num-traits", - "realfft", ] [[package]] @@ -4716,20 +3389,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - [[package]] name = "rusticata-macros" version = "4.1.0" @@ -4837,12 +3496,6 @@ 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 = "scrypt" version = "0.12.0-rc.10" @@ -4959,12 +3612,6 @@ dependencies = [ "digest 0.11.1", ] -[[package]] -name = "sha1dc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdf1713fb0547b120571e5f7b37dc2650dc03d2abf46a18928f54372000947f" - [[package]] name = "sha2" version = "0.10.9" @@ -5058,21 +3705,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - [[package]] name = "slab" version = "0.4.12" @@ -5203,9 +3835,6 @@ dependencies = [ [[package]] name = "sprite" version = "0.1.0" -dependencies = [ - "resvg", -] [[package]] name = "ssh-cipher" @@ -5246,27 +3875,6 @@ dependencies = [ "toyos-tmpdir", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - -[[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 = "subtle" version = "2.6.1" @@ -5291,16 +3899,6 @@ dependencies = [ "toyos-wallclock", ] -[[package]] -name = "svgtypes" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" -dependencies = [ - "kurbo", - "siphasher", -] - [[package]] name = "swap" version = "0.1.0" @@ -5387,19 +3985,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix 1.1.5", - "windows-sys 0.61.2", -] - [[package]] name = "terminal" version = "0.1.0" @@ -5497,38 +4082,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-skia" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinyvec" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" - [[package]] name = "tokio" version = "1.51.1" @@ -5706,11 +4259,10 @@ dependencies = [ "flate2", "fontdue", "getrandom 0.3.4", - "gix", "gpt", - "image", "kernel", "libc", + "libm", "pem", "rcgen", "rustls", @@ -5740,7 +4292,6 @@ dependencies = [ "toyos-wallclock", "toyos-xhci", "ureq", - "uuid", "webpki-root-certs", ] @@ -6190,16 +4741,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - [[package]] name = "ttf-parser" version = "0.21.1" @@ -6212,90 +4753,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "ucs2" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad643914094137d475641b6bab89462505316ec2ce70907ad20102d28a79ab8" -dependencies = [ - "bit_field", -] - -[[package]] -name = "uefi" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ead9f748a4646479b850add36b527113a80e80a7e0f44d7b0334291850dcc5" -dependencies = [ - "bitflags 2.11.0", - "log", - "ptr_meta", - "ucs2", - "uefi-macros", - "uefi-raw", - "uguid", -] - -[[package]] -name = "uefi-macros" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a7b1c2c808c3db854a54d5215e3f7e7aaf5dcfbce095598cba6af29895695d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "uefi-raw" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864ac69eadd877bfb34e7814be1928122ed0057d9f975169a56ee496aa7bdfd7" -dependencies = [ - "bitflags 2.11.0", - "ptr_meta", - "uguid", -] - -[[package]] -name = "uefi-services" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79fcb420624743c895bad0f9480fbc2f64e7c8d8611fb1ada6bdd799942feb4" -dependencies = [ - "cfg-if", - "log", - "uefi", -] - -[[package]] -name = "uguid" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533" - -[[package]] -name = "unicode-bom" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" - [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -6351,7 +4814,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" dependencies = [ - "base64 0.23.1", + "base64", "log", "percent-encoding", "rustls", @@ -6367,7 +4830,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" dependencies = [ - "base64 0.23.1", + "base64", "http", "httparse", "log", @@ -6383,28 +4846,6 @@ dependencies = [ "toyos-xhci", ] -[[package]] -name = "usvg" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "xmlwriter", -] - [[package]] name = "utf8-zero" version = "0.8.1" @@ -7194,12 +5635,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "yasna" version = "0.6.0" @@ -7247,18 +5682,3 @@ 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-jpeg" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml index 20d33e474c8..ae1e1c90e28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,12 +163,9 @@ license = "MIT OR Apache-2.0" default-run = "toyos-build" [dependencies] -fatfs = "0.3.6" fontdue = "0.9" -gpt = "3.1.0" libc = "0.2" toml = "0.8" -uuid = { version = "1", features = ["v4"] } serde = { version = "1", features = ["derive"] } serde_json = "1" bcachefs = { path = "bcachefs" } @@ -205,15 +202,18 @@ toyos-blackbox = { path = "toyos-blackbox" } # The signed image, the slot table and the signer: `src/signing.rs` signs with # the same crate the loader and `/system/bin/update` verify with. toyos-update = { path = "toyos-update", features = ["sign"] } -# A throwaway key's seed, from the operating system. Already resolved here. +# A throwaway key's seed and every GUID a partition table names, from the +# operating system. getrandom = "0.3" +# The wallpaper's `powf` and `expf`, in Rust and the same on every host, where +# `f32`'s own call the host's C library and ROOT's UUID is derived from its bytes. +libm = "0.2.16" # The one `openssh-key-v1` reader and writer, shared with the SSH server's # host key, and the `SHA256:` fingerprint the owner's key is named by. toyos-ssh = { path = "toyos-ssh" } # `classify`, so the host reading a log volume and `logkeeper` writing it cannot # disagree about which files on it are `logkeeper`'s. toyos-wallclock = { path = "toyos-wallclock" } -image = { version = "0.25", default-features = false, features = ["jpeg"] } # Every SHA-256 the build takes: `NOTICE`'s record of a committed file, a # store key, a ROOT's name, a release asset's digest, and the harness's of a # body a guest fetches over TLS. @@ -227,15 +227,16 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"] # takes (`issues/the-internet-clients-work-unchanged.md`), whose C # and assembly the host's `cc` compiles. ureq = { version = "3", default-features = false, features = ["rustls"] } -# The commit an image is built from, its time and whether the tree is that -# commit's, where `git` is a host's C tool (`src/build.rs`'s `release`). -gix = { version = "0.88", default-features = false, features = ["status", "sha1"] } # The authorities `build::trust_roots` writes to ROOT, as the rustls project # publishes the Mozilla root program, and the PEM it writes them in. webpki-root-certs = "1" pem = "4" [dev-dependencies] +# The FAT implementation `src/fatformat.rs` is judged against, byte for byte. +fatfs = "0.3.6" +# The GPT writer `src/gptwrite.rs` is judged against, byte for byte. +gpt = "3.1.0" # `USER_TOP`, so the harness judges a held `rsp` against the bound the kernel # checks every user pointer against and not against a second copy of it. toyos-userbound = { path = "toyos-userbound" } diff --git a/NOTICE b/NOTICE index d4bd1850233..b4de8c10451 100644 --- a/NOTICE +++ b/NOTICE @@ -185,7 +185,8 @@ would have arrived unremarked. `assets/` is walked whole for that reason. MIT requires the notice to be included in copies. Before this file the only copy of it in reach was a `phosphor-icons/` directory on one developer's disk, which `.gitignore` line 7 keeps out of the repository. They ship as -`share/icons/*.svg` on every ROOT that carries `assets/`. +`share/icons/*.alpha`, drawn from them at build time by `src/icons.rs`, on +every ROOT that carries `assets/`. assets/fonts/OpenSans-*.ttf — Open Sans, SIL Open Font License 1.1 diff --git a/README.md b/README.md index b95702c526b..9bc02b2afbd 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ Licensed under either of at your option. Some of what the repository carries is not ours and is under other terms: -`userland/doom` is GPL-2.0, `assets/` holds a font, a set of icons, a wallpaper +`userland/doom` is GPL-2.0, `assets/` holds a font, a set of icons and id Software's Doom shareware IWAD, and `tests/testcases/` holds TinyCC's test corpus. Third-party crates keep their own upstream licenses. **[NOTICE](NOTICE) is the list**, item by item, diff --git a/assets/wallpaper.jpg b/assets/wallpaper.jpg deleted file mode 100644 index b0863088b14..00000000000 Binary files a/assets/wallpaper.jpg and /dev/null differ diff --git a/bootloader/Cargo.toml b/bootloader/Cargo.toml index f8e0f7d7e07..c61391203ea 100644 --- a/bootloader/Cargo.toml +++ b/bootloader/Cargo.toml @@ -40,7 +40,3 @@ toyos-update = { path = "../toyos-update" } # soft-float, and the x86 backend reaches for SSE and SHA-NI registers a UEFI # application may not assume are its own. sha2 = { version = "0.10", default-features = false, features = ["force-soft"] } -# `alloc`: `variable_keys` and `get_variable_boxed`, which are how the boot -# entry pointing at this image is found among the firmware's own variables. -uefi = { version = "0.26.0", default-features = false, features = ["alloc"] } -uefi-services = { version = "0.23.0", features = ["panic_handler", "logger"] } diff --git a/bootloader/src/attempt.rs b/bootloader/src/attempt.rs index 069705f9ce0..64640669762 100644 --- a/bootloader/src/attempt.rs +++ b/bootloader/src/attempt.rs @@ -7,9 +7,7 @@ use alloc::string::String; use toyos_update::record::{self, Record}; -use uefi::proto::media::file::{Directory, File, FileAttribute, FileMode}; -use uefi::prelude::*; -use uefi::{cstr16, CStr16}; +use crate::efi::{cstr16, CStr16, File, Mode, Status, SystemTable}; use crate::loaderlog; @@ -25,12 +23,12 @@ const NAME: &CStr16 = cstr16!("attempts"); /// **`Err` is not zero.** A volume this cannot read is one the bound is off on, /// and the caller says so rather than treating an unreadable stick as a first /// attempt — which would be a bound that silently never fires. -pub fn read(system_table: &SystemTable, guid: &[u8; 16]) -> Result { +pub fn read(system_table: &SystemTable, guid: &[u8; 16]) -> Result { loaderlog::with_volume(system_table, guid, |root| { - let file = match root.open(NAME, FileMode::Read, FileAttribute::empty()) { + let file = match root.open(NAME, Mode::Read) { Ok(file) => file, // No file is a first attempt, which is every freshly flashed image. - Err(e) if e.status() == Status::NOT_FOUND => return Ok(Record::default()), + Err(Status::NOT_FOUND) => return Ok(Record::default()), Err(e) => return Err(alloc::format!("{NAME} would not open ({e})")), }; let Some(mut file) = file.into_regular_file() else { @@ -50,7 +48,7 @@ pub fn read(system_table: &SystemTable, guid: &[u8; 16]) -> Result, guid: &[u8; 16], record: &Record) -> Result<(), String> { +pub fn write(system_table: &SystemTable, guid: &[u8; 16], record: &Record) -> Result<(), String> { loaderlog::with_volume(system_table, guid, |root| put(root, guid, record)).and_then(|inner| inner) } @@ -60,25 +58,25 @@ pub fn write_chosen(guid: &[u8; 16], record: &Record) -> Result<(), String> { loaderlog::with_open_volume(|root| put(root, guid, record)).and_then(|inner| inner) } -fn put(root: &mut Directory, guid: &[u8; 16], record: &Record) -> Result<(), String> { +fn put(root: &mut File, guid: &[u8; 16], record: &Record) -> Result<(), String> { // Deleted and recreated rather than rewound: a fixed-width record is // still a record a shorter write would leave the tail of. - match root.open(NAME, FileMode::ReadWrite, FileAttribute::empty()) { + match root.open(NAME, Mode::ReadWrite) { Ok(stale) => { if let Err(e) = stale.delete() { return Err(alloc::format!("{NAME} would not delete ({e})")); } } - Err(e) if e.status() == Status::NOT_FOUND => {} + Err(Status::NOT_FOUND) => {} Err(e) => return Err(alloc::format!("{NAME} would not open ({e})")), } let file = root - .open(NAME, FileMode::CreateReadWrite, FileAttribute::empty()) + .open(NAME, Mode::CreateReadWrite) .map_err(|e| alloc::format!("{NAME} would not be created ({e})"))?; let Some(mut file) = file.into_regular_file() else { return Err(alloc::format!("{NAME} on the log partition is a directory")); }; - file.write(&record.encode(guid)).map_err(|e| alloc::format!("{NAME} would not write ({e})"))?; + file.write(&record.encode(guid)).map_err(|(e, _)| alloc::format!("{NAME} would not write ({e})"))?; // **Flushed here and not at the handoff.** What this file exists to // survive is a power cut, and a byte in a cache survives nothing. file.flush().map_err(|e| alloc::format!("{NAME} would not flush ({e})"))?; diff --git a/bootloader/src/blackbox.rs b/bootloader/src/blackbox.rs index 0f749810338..7ddf8fc8768 100644 --- a/bootloader/src/blackbox.rs +++ b/bootloader/src/blackbox.rs @@ -16,8 +16,7 @@ use alloc::string::String; use alloc::vec::Vec; -use uefi::prelude::*; -use uefi::table::boot::{AllocateType, MemoryType}; +use crate::efi::{AllocateType, SystemTable}; use toyos_blackbox::{BYTES, PHYS, State}; use toyos_wallclock::Civil; @@ -53,12 +52,8 @@ pub struct Page(u64); /// last one's file or replaces it is what the page decides — so a refusal is /// returned as a line for the caller to write rather than printed here, where a /// machine with no console would lose it. -pub fn claim(system_table: &SystemTable) -> (Option, Option) { - match system_table.boot_services().allocate_pages( - AllocateType::Address(PHYS), - MemoryType::LOADER_DATA, - toyos_blackbox::PAGES, - ) { +pub fn claim(system_table: &SystemTable) -> (Option, Option) { + match system_table.boot_services().allocate_pages(AllocateType::Address(PHYS), toyos_blackbox::PAGES) { Ok(at) => { // `AllocateType::Address` allocates that address or fails; firmware // answering with another one has not done what was asked of it. diff --git a/bootloader/src/bootnext.rs b/bootloader/src/bootnext.rs index b38e223a8bc..4a19e33eadc 100644 --- a/bootloader/src/bootnext.rs +++ b/bootloader/src/bootnext.rs @@ -15,12 +15,7 @@ //! that booted us from a removable-media fallback path has no entry of ours at //! all and must be told so rather than have one guessed at. -use uefi::prelude::*; -use uefi::proto::device_path::media::PartitionSignature; -use uefi::proto::device_path::{DevicePath, DeviceSubType, DeviceType}; -use uefi::proto::loaded_image::LoadedImage; -use uefi::table::runtime::{VariableAttributes, VariableVendor}; -use uefi::CStr16; +use crate::efi::{cstr16, CStr16, DevicePath, Handle, HardDrive, LoadedImage, SystemTable, VariableAttributes, GLOBAL_VARIABLE}; /// The head of every line this module writes. const HEAD: &str = "Boot chain:"; @@ -39,7 +34,7 @@ const LOAD_OPTION_HEAD: usize = 6; /// A refusal is not a failure of the boot: the kernel still runs and still seals /// its page. What is lost is the *next* boot, so the line says exactly that /// rather than reporting a variable write. -pub fn point_at_us(handle: Handle, system_table: &SystemTable) { +pub fn point_at_us(handle: Handle, system_table: &SystemTable) { let Some(ours) = our_partition(handle, system_table) else { return println!( "{HEAD} firmware did not load this image off a GPT partition, so there is no entry \ @@ -54,7 +49,7 @@ pub fn point_at_us(handle: Handle, system_table: &SystemTable) { }; let write = system_table.runtime_services().set_variable( cstr16!("BootNext"), - &VariableVendor::GLOBAL_VARIABLE, + &GLOBAL_VARIABLE, // Non-volatile, because it has to survive the reset that is the whole point. VariableAttributes::NON_VOLATILE | VariableAttributes::BOOTSERVICE_ACCESS @@ -71,24 +66,21 @@ pub fn point_at_us(handle: Handle, system_table: &SystemTable) { } /// The GPT partition GUID of the volume firmware loaded this image from. -fn our_partition(handle: Handle, system_table: &SystemTable) -> Option<[u8; 16]> { +fn our_partition(handle: Handle, system_table: &SystemTable) -> Option<[u8; 16]> { let bs = system_table.boot_services(); - let image = crate::protocol::exclusive::(bs, handle).ok()?; + let image = bs.exclusive::(handle).ok()?; let device = image.device()?; - let path = crate::protocol::exclusive::(bs, device).ok()?; - hard_drive_guid(path.node_iter()) + let path = bs.exclusive::(device).ok()?; + hard_drive_guid(path.nodes()) } /// The GPT signature of the first HARDDRIVE node in a device path, or `None` /// where the path has none — a network boot, or a disk with no GPT. -fn hard_drive_guid<'a>(nodes: impl Iterator) -> Option<[u8; 16]> { - for node in nodes { - if node.full_type() != (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) { - continue; - } - let hd = <&uefi::proto::device_path::media::HardDrive>::try_from(node).ok()?; - if let PartitionSignature::Guid(guid) = hd.partition_signature() { - return Some(guid.to_bytes()); +fn hard_drive_guid<'a>(nodes: impl Iterator) -> Option<[u8; 16]> { + for node in nodes.filter(|node| (node[0], node[1]) == HardDrive::TYPE) { + let hd = HardDrive::parse(node)?; + if hd.signature_type == HardDrive::GUID_SIGNATURE { + return Some(hd.signature); } } None @@ -98,17 +90,16 @@ fn hard_drive_guid<'a>(nodes: impl Iterator, ours: &[u8; 16]) -> Option { +fn entry_for(system_table: &SystemTable, ours: &[u8; 16]) -> Option { let rt = system_table.runtime_services(); let keys = rt.variable_keys().ok()?; let mut found: Option = None; - for key in keys { - if key.vendor != VariableVendor::GLOBAL_VARIABLE { + for (name, vendor) in keys { + if vendor != GLOBAL_VARIABLE { continue; } - let Ok(name) = key.name() else { continue }; - let Some(number) = entry_number(name) else { continue }; - let Ok((bytes, _)) = rt.get_variable_boxed(name, &key.vendor) else { continue }; + let Some(number) = entry_number(&name) else { continue }; + let Ok((bytes, _)) = rt.get_variable(&name, &vendor) else { continue }; if !load_option_names(&bytes, ours) { continue; } @@ -121,7 +112,7 @@ fn entry_for(system_table: &SystemTable, ours: &[u8; 16]) -> Option { /// `Boot0003` is entry 3; anything else here is some other global variable. fn entry_number(name: &CStr16) -> Option { - let mut chars = name.iter().map(|c| char::from(*c)); + let mut chars = name.units().iter().map(|&unit| char::from_u32(u32::from(unit)).unwrap_or(char::REPLACEMENT_CHARACTER)); for want in ENTRY_PREFIX.chars() { if chars.next()? != want { return None; diff --git a/bootloader/src/efi/boot.rs b/bootloader/src/efi/boot.rs new file mode 100644 index 00000000000..8009db982e2 --- /dev/null +++ b/bootloader/src/efi/boot.rs @@ -0,0 +1,402 @@ +//! `EFI_BOOT_SERVICES` (UEFI 2.10 §4.4) and what this loader calls of them: +//! pages and pool, the memory map, the watchdog, handles, protocols, and the +//! exit. +//! +//! **A protocol is opened two ways and no other.** EXCLUSIVE calls `Stop` on +//! every driver holding the protocol BY_DRIVER (UEFI 2.10 §7.3.9, +//! `OpenProtocol()`), and on `GraphicsOutput` that is the firmware's graphics +//! console, whose screen the loader's own lines are on. [`BootServices::get`] +//! opens GET_PROTOCOL, which stops nothing; [`BootServices::exclusive`] opens +//! only an [`Exclusive`] protocol, one no firmware console drives. + +use core::ffi::c_void; +use core::mem::offset_of; +use core::ptr::{self, NonNull}; + +use super::{Guid, Handle, Status, TableHeader}; + +/// Every page UEFI counts (§7.2.1). +pub const PAGE_SIZE: usize = 4096; + +/// `EfiLoaderData` (§7.2.1): what this loader allocates, and what the +/// firmware gives an application's data. +const LOADER_DATA: u32 = 2; + +type Unused = *const c_void; + +/// `EFI_BOOT_SERVICES` (§4.4), in the spec's own field order. +#[repr(C)] +pub struct BootServices { + hdr: TableHeader, + raise_tpl: Unused, + restore_tpl: Unused, + allocate_pages: unsafe extern "efiapi" fn(kind: u32, memory: u32, pages: usize, at: *mut u64) -> Status, + free_pages: unsafe extern "efiapi" fn(at: u64, pages: usize) -> Status, + get_memory_map: unsafe extern "efiapi" fn( + size: *mut usize, + map: *mut u64, + key: *mut usize, + descriptor_size: *mut usize, + descriptor_version: *mut u32, + ) -> Status, + allocate_pool: unsafe extern "efiapi" fn(memory: u32, size: usize, at: *mut *mut u8) -> Status, + free_pool: unsafe extern "efiapi" fn(at: *mut u8) -> Status, + create_event: Unused, + set_timer: Unused, + wait_for_event: Unused, + signal_event: Unused, + close_event: Unused, + check_event: Unused, + install_protocol_interface: Unused, + reinstall_protocol_interface: Unused, + uninstall_protocol_interface: Unused, + handle_protocol: Unused, + reserved: Unused, + register_protocol_notify: Unused, + locate_handle: Unused, + locate_device_path: Unused, + install_configuration_table: Unused, + load_image: Unused, + start_image: Unused, + exit: Unused, + unload_image: Unused, + exit_boot_services: unsafe extern "efiapi" fn(image: *mut c_void, key: usize) -> Status, + get_next_monotonic_count: Unused, + stall: Unused, + set_watchdog_timer: + unsafe extern "efiapi" fn(seconds: usize, code: u64, data_size: usize, data: *const u16) -> Status, + connect_controller: Unused, + disconnect_controller: Unused, + open_protocol: unsafe extern "efiapi" fn( + handle: *mut c_void, + protocol: *const Guid, + interface: *mut *mut c_void, + agent: *mut c_void, + controller: *mut c_void, + attributes: u32, + ) -> Status, + close_protocol: unsafe extern "efiapi" fn( + handle: *mut c_void, + protocol: *const Guid, + agent: *mut c_void, + controller: *mut c_void, + ) -> Status, + open_protocol_information: Unused, + protocols_per_handle: Unused, + locate_handle_buffer: unsafe extern "efiapi" fn( + search: u32, + protocol: *const Guid, + key: *const c_void, + count: *mut usize, + buffer: *mut *mut Handle, + ) -> Status, + locate_protocol: Unused, + install_multiple_protocol_interfaces: Unused, + uninstall_multiple_protocol_interfaces: Unused, + calculate_crc32: Unused, + copy_mem: Unused, + set_mem: Unused, + create_event_ex: Unused, +} + +const _: () = { + assert!(size_of::() == 24 + 44 * 8); + assert!(offset_of!(BootServices, allocate_pages) == 40); + assert!(offset_of!(BootServices, free_pages) == 48); + assert!(offset_of!(BootServices, get_memory_map) == 56); + assert!(offset_of!(BootServices, allocate_pool) == 64); + assert!(offset_of!(BootServices, free_pool) == 72); + assert!(offset_of!(BootServices, exit_boot_services) == 232); + assert!(offset_of!(BootServices, set_watchdog_timer) == 256); + assert!(offset_of!(BootServices, open_protocol) == 280); + assert!(offset_of!(BootServices, close_protocol) == 288); + assert!(offset_of!(BootServices, locate_handle_buffer) == 312); +}; + +/// `EFI_ALLOCATE_TYPE` (§7.2.1), as this loader asks. +pub enum AllocateType { + AnyPages, + Address(u64), +} + +/// `OpenProtocol`'s two attributes this loader passes (§7.3.9). +const GET_PROTOCOL: u32 = 0x02; +const EXCLUSIVE: u32 = 0x20; + +/// `ByProtocol` of `EFI_LOCATE_SEARCH_TYPE` (§7.3.15). +const BY_PROTOCOL: u32 = 2; + +/// An interface firmware installs on a handle. +/// +/// # Safety +/// `Self` is the layout of the interface installed under [`Protocol::GUID`]. +pub unsafe trait Protocol { + const GUID: Guid; +} + +/// A protocol this loader may hold EXCLUSIVE: one no firmware console drives. +pub trait Exclusive: Protocol {} + +/// A protocol open on a handle, closed when this drops. +pub struct Scoped<'a, P: Protocol> { + bs: &'a BootServices, + handle: Handle, + interface: NonNull

, +} + +impl core::ops::Deref for Scoped<'_, P> { + type Target = P; + fn deref(&self) -> &P { + // SAFETY: firmware's interface, installed under `P::GUID` and held + // open for as long as `self`. + unsafe { self.interface.as_ref() } + } +} + +impl Scoped<'_, P> { + /// The interface as firmware's functions take it, `This`. + pub fn this(&self) -> *mut P { + self.interface.as_ptr() + } +} + +impl Drop for Scoped<'_, P> { + fn drop(&mut self) { + // SAFETY: the open this undoes, under the same agent. + let status = unsafe { + (self.bs.close_protocol)(self.handle.as_ptr(), &P::GUID, super::image().as_ptr(), ptr::null_mut()) + }; + // Only a close that names another open fails, and this names its own. + assert!(status.is_success(), "CloseProtocol({}) answered {status}", P::GUID); + } +} + +/// The handles `LocateHandleBuffer` answered, in its pool buffer. +pub struct Handles<'a> { + bs: &'a BootServices, + at: NonNull, + count: usize, +} + +impl core::ops::Deref for Handles<'_> { + type Target = [Handle]; + fn deref(&self) -> &[Handle] { + // SAFETY: firmware answered `count` handles at `at`. + unsafe { core::slice::from_raw_parts(self.at.as_ptr(), self.count) } + } +} + +impl Drop for Handles<'_> { + fn drop(&mut self) { + // SAFETY: the pool buffer `LocateHandleBuffer` allocated for this answer. + let freed = unsafe { self.bs.free_pool(self.at.as_ptr().cast()) }; + assert!(freed.is_ok(), "firmware would not take back the handle buffer it gave"); + } +} + +/// `EFI_MEMORY_DESCRIPTOR` (§7.2.3). Firmware's stride is its own +/// `DescriptorSize`, never this struct's size. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct MemoryDescriptor { + pub ty: u32, + pub phys_start: u64, + pub virt_start: u64, + pub page_count: u64, + pub att: u64, +} + +const _: () = { + assert!(size_of::() == 40); + assert!(offset_of!(MemoryDescriptor, phys_start) == 8); + assert!(offset_of!(MemoryDescriptor, page_count) == 24); + assert!(offset_of!(MemoryDescriptor, att) == 32); +}; + +impl MemoryDescriptor { + /// `EfiMemoryMappedIO` and `EfiMemoryMappedIOPortSpace` (§7.2.1). + pub const MMIO: u32 = 11; + pub const MMIO_PORT_SPACE: u32 = 12; + /// `EFI_MEMORY_WB` (§7.2.3). + pub const WRITE_BACK: u64 = 0x8; +} + +/// What `GetMemoryMap` wrote: its key and its extent. +#[derive(Clone, Copy)] +pub(super) struct Filled { + pub(super) key: usize, + size: usize, + entry_size: usize, +} + +/// The memory map in the buffer it was taken into. +pub struct MemoryMap<'a> { + words: &'a [u64], + filled: Filled, +} + +impl<'a> MemoryMap<'a> { + pub(super) fn new(words: &'a [u64], filled: Filled) -> Self { + MemoryMap { words, filled } + } + + pub fn entries(&self) -> Entries<'_> { + Entries { map: self, next: 0 } + } +} + +/// A [`MemoryMap`]'s descriptors, in firmware's order. +pub struct Entries<'a> { + map: &'a MemoryMap<'a>, + next: usize, +} + +impl Iterator for Entries<'_> { + type Item = MemoryDescriptor; + + fn next(&mut self) -> Option { + let Filled { size, entry_size, .. } = self.map.filled; + if self.next >= size / entry_size { + return None; + } + let at = self.next * entry_size; + self.next += 1; + // SAFETY: `fill_memory_map` checked the descriptors lie inside the + // buffer and that each is at least a `MemoryDescriptor` long. + Some(unsafe { self.map.words.as_ptr().cast::().add(at).cast::().read_unaligned() }) + } +} + +impl BootServices { + /// `AllocatePages` (§7.2.1), as `EfiLoaderData`. + pub fn allocate_pages(&self, kind: AllocateType, pages: usize) -> Result { + let (kind, mut at) = match kind { + AllocateType::AnyPages => (0, 0), + AllocateType::Address(at) => (2, at), + }; + // SAFETY: the out parameter is a live `u64`. + unsafe { (self.allocate_pages)(kind, LOADER_DATA, pages, &mut at) }.ok().map(|()| at) + } + + /// `FreePages` (§7.2.2). + /// + /// # Safety + /// `at` and `pages` are an allocation `allocate_pages` made, and nothing + /// reads or writes it after. + pub unsafe fn free_pages(&self, at: u64, pages: usize) -> Result<(), Status> { + // SAFETY: the caller's contract. + unsafe { (self.free_pages)(at, pages) }.ok() + } + + /// `AllocatePool` (§7.2.4), as `EfiLoaderData`: 8-byte aligned. + pub fn allocate_pool(&self, size: usize) -> Result<*mut u8, Status> { + let mut at = ptr::null_mut(); + // SAFETY: the out parameter is a live pointer. + unsafe { (self.allocate_pool)(LOADER_DATA, size, &mut at) }.ok().map(|()| at) + } + + /// `FreePool` (§7.2.5). + /// + /// # Safety + /// `at` is a pool allocation of firmware's, and nothing reads or writes it + /// after. + pub unsafe fn free_pool(&self, at: *mut u8) -> Result<(), Status> { + // SAFETY: the caller's contract. + unsafe { (self.free_pool)(at) }.ok() + } + + /// The bytes the memory map takes now, and the size of one descriptor. + pub fn memory_map_size(&self) -> (usize, usize) { + let (mut size, mut key, mut entry_size, mut version) = (0, 0, 0, 0); + // SAFETY: a zero size with no buffer asks only for the size (§7.2.3). + let status = + unsafe { (self.get_memory_map)(&mut size, ptr::null_mut(), &mut key, &mut entry_size, &mut version) }; + assert!(status == Status::BUFFER_TOO_SMALL, "GetMemoryMap with no buffer answered {status}"); + (size, entry_size) + } + + /// The memory map, taken into `words`. + pub fn memory_map<'b>(&self, words: &'b mut [u64]) -> Result, Status> { + let filled = self.fill_memory_map(words)?; + Ok(MemoryMap::new(words, filled)) + } + + pub(super) fn fill_memory_map(&self, words: &mut [u64]) -> Result { + let capacity = size_of_val(words); + let (mut size, mut key, mut entry_size, mut version) = (capacity, 0, 0, 0); + // SAFETY: `words` is `size` writable bytes, aligned for a descriptor. + unsafe { (self.get_memory_map)(&mut size, words.as_mut_ptr(), &mut key, &mut entry_size, &mut version) } + .ok()?; + assert!( + size <= capacity && entry_size >= size_of::(), + "GetMemoryMap answered {size} bytes of {entry_size}-byte descriptors into {capacity}" + ); + Ok(Filled { key, size, entry_size }) + } + + /// `ExitBootServices` (§7.4.6). + pub(super) fn exit(&self, image: Handle, key: usize) -> Status { + // SAFETY: the call is the spec's; the caller stops using boot services + // when it succeeds. + unsafe { (self.exit_boot_services)(image.as_ptr(), key) } + } + + /// This image's handle. + pub fn image_handle(&self) -> Handle { + super::image() + } + + /// `SetWatchdogTimer` (§7.5.1) with no data. + pub fn set_watchdog_timer(&self, seconds: usize, code: u64) -> Result<(), Status> { + // SAFETY: no data, so no pointer is read. + unsafe { (self.set_watchdog_timer)(seconds, code, 0, ptr::null()) }.ok() + } + + /// Every handle carrying `P` (`LocateHandleBuffer`, §7.3.15). + pub fn handles(&self) -> Result, Status> { + let (mut count, mut at) = (0usize, ptr::null_mut()); + // SAFETY: both out parameters are live. + unsafe { (self.locate_handle_buffer)(BY_PROTOCOL, &P::GUID, ptr::null(), &mut count, &mut at) }.ok()?; + let at = NonNull::new(at).expect("LocateHandleBuffer answered success with no buffer"); + Ok(Handles { bs: self, at, count }) + } + + /// The first handle carrying `P`. + pub fn handle_for(&self) -> Result { + self.handles::

()?.first().copied().ok_or(Status::NOT_FOUND) + } + + /// `P` on `handle`, GET_PROTOCOL: it stops no driver. + /// + /// Nothing between the open and the drop can uninstall the protocol: the + /// loader is the one image running, it registers no event callback, and it + /// calls no boot service that connects or disconnects a controller. + pub fn get(&self, handle: Handle) -> Result, Status> { + self.open(handle, GET_PROTOCOL) + } + + /// `P` on `handle`, EXCLUSIVE. + pub fn exclusive(&self, handle: Handle) -> Result, Status> { + self.open(handle, EXCLUSIVE) + } + + fn open(&self, handle: Handle, attributes: u32) -> Result, Status> { + let mut interface = ptr::null_mut(); + // SAFETY: the out parameter is live, and the agent is this image. + unsafe { + (self.open_protocol)( + handle.as_ptr(), + &P::GUID, + &mut interface, + super::image().as_ptr(), + ptr::null_mut(), + attributes, + ) + } + .ok()?; + let interface = NonNull::new(interface.cast::

()) + .unwrap_or_else(|| panic!("OpenProtocol({}) answered success with no interface", P::GUID)); + Ok(Scoped { bs: self, handle, interface }) + } +} + diff --git a/bootloader/src/efi/mod.rs b/bootloader/src/efi/mod.rs new file mode 100644 index 00000000000..b0525001e80 --- /dev/null +++ b/bootloader/src/efi/mod.rs @@ -0,0 +1,560 @@ +//! The UEFI this loader calls, and nothing it does not: UEFI 2.10's tables, +//! protocols, GUIDs and status codes, each laid out from the section cited at +//! it and held there by compile-time `size_of`/`offset_of!` asserts, so a field +//! added, dropped or resized fails the build of both targets. +//! +//! Firmware is the loader's host and is trusted to keep its own contracts; a +//! length it writes back about a buffer of this side's is still held to that +//! buffer. The safe wrappers each remove a hazard that is this side's: a +//! name handed over without its NUL, a protocol left open or opened in a way +//! that stops a driver, a boot service called once `ExitBootServices` has run. +//! +//! **Boot services end at [`SystemTable::exit_boot_services`]**, which consumes +//! the table; the console and the allocator, which reach firmware without it, +//! refuse from then on. + +mod boot; +mod proto; +mod runtime; + +use core::ffi::c_void; +use core::fmt; +use core::ptr::{self, NonNull}; +use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; + +use alloc::vec::Vec; + +pub use boot::{AllocateType, BootServices, MemoryDescriptor, MemoryMap, Scoped, PAGE_SIZE}; +pub use proto::{ + BlockIo, DevicePath, File, Gop, HardDrive, LoadedImage, Mode, PartitionInfo, PciRootBridgeIo, + PixelFormat, Rng, SimpleFileSystem, +}; +pub use runtime::{ResetType, RuntimeServices, Time, VariableAttributes, GLOBAL_VARIABLE}; + +/// `EFI_STATUS` (UEFI 2.10 Appendix D): success, a warning, or an error, +/// which carries the top bit. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +#[must_use] +pub struct Status(pub usize); + +const ERROR: usize = 1 << (usize::BITS - 1); + +/// Appendix D's codes by name: every refusal this loader writes carries one. +const NAMES: [(usize, &str); 41] = [ + (0, "SUCCESS"), + (1, "WARN_UNKNOWN_GLYPH"), + (2, "WARN_DELETE_FAILURE"), + (3, "WARN_WRITE_FAILURE"), + (4, "WARN_BUFFER_TOO_SMALL"), + (5, "WARN_STALE_DATA"), + (6, "WARN_FILE_SYSTEM"), + (7, "WARN_RESET_REQUIRED"), + (ERROR | 1, "LOAD_ERROR"), + (ERROR | 2, "INVALID_PARAMETER"), + (ERROR | 3, "UNSUPPORTED"), + (ERROR | 4, "BAD_BUFFER_SIZE"), + (ERROR | 5, "BUFFER_TOO_SMALL"), + (ERROR | 6, "NOT_READY"), + (ERROR | 7, "DEVICE_ERROR"), + (ERROR | 8, "WRITE_PROTECTED"), + (ERROR | 9, "OUT_OF_RESOURCES"), + (ERROR | 10, "VOLUME_CORRUPTED"), + (ERROR | 11, "VOLUME_FULL"), + (ERROR | 12, "NO_MEDIA"), + (ERROR | 13, "MEDIA_CHANGED"), + (ERROR | 14, "NOT_FOUND"), + (ERROR | 15, "ACCESS_DENIED"), + (ERROR | 16, "NO_RESPONSE"), + (ERROR | 17, "NO_MAPPING"), + (ERROR | 18, "TIMEOUT"), + (ERROR | 19, "NOT_STARTED"), + (ERROR | 20, "ALREADY_STARTED"), + (ERROR | 21, "ABORTED"), + (ERROR | 22, "ICMP_ERROR"), + (ERROR | 23, "TFTP_ERROR"), + (ERROR | 24, "PROTOCOL_ERROR"), + (ERROR | 25, "INCOMPATIBLE_VERSION"), + (ERROR | 26, "SECURITY_VIOLATION"), + (ERROR | 27, "CRC_ERROR"), + (ERROR | 28, "END_OF_MEDIA"), + (ERROR | 31, "END_OF_FILE"), + (ERROR | 32, "INVALID_LANGUAGE"), + (ERROR | 33, "COMPROMISED_DATA"), + (ERROR | 34, "IP_ADDRESS_CONFLICT"), + (ERROR | 35, "HTTP_ERROR"), +]; + +impl Status { + pub const SUCCESS: Status = Status(0); + pub const BUFFER_TOO_SMALL: Status = Status(ERROR | 5); + pub const NOT_FOUND: Status = Status(ERROR | 14); + pub const ABORTED: Status = Status(ERROR | 21); + + pub fn is_success(self) -> bool { + self == Self::SUCCESS + } + + /// `Ok` for `SUCCESS` alone: a warning is not what was asked for, and is + /// refused like an error. + pub fn ok(self) -> Result<(), Status> { + if self.is_success() { Ok(()) } else { Err(self) } + } +} + +impl fmt::Debug for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match NAMES.iter().find(|(code, _)| *code == self.0) { + Some((_, name)) => f.write_str(name), + None => write!(f, "Status({:#x})", self.0), + } + } +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +/// `EFI_GUID` (UEFI 2.10 Appendix A): its three leading fields little-endian, +/// as it is held in memory and on a GPT disk. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(C, align(4))] +pub struct Guid([u8; 16]); + +const _: () = assert!(size_of::() == 16); + +impl Guid { + /// The GUID the specification prints as `{a, b, c, {d...}}`. + pub const fn new(a: u32, b: u16, c: u16, d: [u8; 8]) -> Self { + let (a, b, c) = (a.to_le_bytes(), b.to_le_bytes(), c.to_le_bytes()); + Guid([a[0], a[1], a[2], a[3], b[0], b[1], c[0], c[1], d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]]) + } +} + +impl fmt::Display for Guid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let b = &self.0; + write!( + f, + "{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + u32::from_le_bytes([b[0], b[1], b[2], b[3]]), + u16::from_le_bytes([b[4], b[5]]), + u16::from_le_bytes([b[6], b[7]]), + b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], + ) + } +} + +/// `EFI_HANDLE` (UEFI 2.10 §2.3.1): opaque, and never null where firmware +/// hands one back. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct Handle(NonNull); + +impl Handle { + fn from_ptr(ptr: *mut c_void) -> Option { + NonNull::new(ptr).map(Handle) + } + + fn as_ptr(self) -> *mut c_void { + self.0.as_ptr() + } +} + +/// A NUL-terminated `CHAR16` string (UEFI 2.10 §2.3.1), the only form a name +/// crosses to firmware in: the type is what keeps firmware from reading past +/// one handed over without its terminator. +#[repr(transparent)] +pub struct CStr16([u16]); + +impl CStr16 { + /// `units` as a name, where its last unit is its only NUL. + pub const fn from_units(units: &[u16]) -> Option<&CStr16> { + let Some((last, body)) = units.split_last() else { return None }; + if *last != 0 { + return None; + } + let mut i = 0; + while i < body.len() { + if body[i] == 0 { + return None; + } + i += 1; + } + // SAFETY: `CStr16` is `repr(transparent)` over `[u16]`. + Some(unsafe { &*(units as *const [u16] as *const CStr16) }) + } + + fn as_ptr(&self) -> *const u16 { + self.0.as_ptr() + } + + /// The units before the NUL. + pub fn units(&self) -> &[u16] { + &self.0[..self.0.len() - 1] + } +} + +impl fmt::Display for CStr16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for unit in self.units() { + fmt::Write::write_char(f, char::from_u32(u32::from(*unit)).unwrap_or(char::REPLACEMENT_CHARACTER))?; + } + Ok(()) + } +} + +/// An owned [`CStr16`]. +pub struct CString16(Vec); + +impl CString16 { + /// `text` as a name, or `None` where it holds a NUL or a character past + /// UCS-2, which `CHAR16` cannot carry. + pub fn new(text: &str) -> Option { + let mut units = Vec::with_capacity(text.len() + 1); + for ch in text.chars() { + let unit = u16::try_from(u32::from(ch)).ok().filter(|&u| u != 0)?; + units.push(unit); + } + units.push(0); + Some(CString16(units)) + } +} + +impl core::ops::Deref for CString16 { + type Target = CStr16; + fn deref(&self) -> &CStr16 { + CStr16::from_units(&self.0).expect("a CString16 ends in its only NUL") + } +} + +impl fmt::Display for CString16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} + +/// `text`, ASCII, as `CHAR16` units with the NUL: [`cstr16!`]'s body. +pub const fn ucs2(text: &str) -> [u16; N] { + let bytes = text.as_bytes(); + assert!(bytes.len() + 1 == N, "the array is the text and its NUL"); + let mut units = [0u16; N]; + let mut i = 0; + while i < bytes.len() { + assert!(bytes[i] != 0 && bytes[i] < 0x80, "a literal name is ASCII with no NUL"); + units[i] = bytes[i] as u16; + i += 1; + } + units +} + +/// A literal name as a `&'static CStr16`, checked at compile time. +macro_rules! cstr16 { + ($text:literal) => {{ + const UNITS: [u16; $text.len() + 1] = $crate::efi::ucs2($text); + const NAME: &$crate::efi::CStr16 = match $crate::efi::CStr16::from_units(&UNITS) { + Some(name) => name, + None => panic!("a literal name is its text and one NUL"), + }; + NAME + }}; +} +pub(crate) use cstr16; + +/// `EFI_TABLE_HEADER` (UEFI 2.10 §4.2). +#[repr(C)] +struct TableHeader { + signature: u64, + revision: u32, + header_size: u32, + crc32: u32, + reserved: u32, +} + +const _: () = assert!(size_of::() == 24); + +/// `EFI_SYSTEM_TABLE` (UEFI 2.10 §4.3). +#[repr(C)] +struct RawSystemTable { + hdr: TableHeader, + firmware_vendor: *const u16, + firmware_revision: u32, + console_in_handle: *mut c_void, + con_in: *mut c_void, + console_out_handle: *mut c_void, + con_out: *mut proto::TextOutput, + standard_error_handle: *mut c_void, + std_err: *mut c_void, + runtime_services: *const RuntimeServices, + boot_services: *const BootServices, + number_of_table_entries: usize, + configuration_table: *const ConfigurationTable, +} + +const _: () = { + assert!(size_of::() == 120); + assert!(core::mem::offset_of!(RawSystemTable, firmware_revision) == 32); + assert!(core::mem::offset_of!(RawSystemTable, con_out) == 64); + assert!(core::mem::offset_of!(RawSystemTable, runtime_services) == 88); + assert!(core::mem::offset_of!(RawSystemTable, boot_services) == 96); + assert!(core::mem::offset_of!(RawSystemTable, number_of_table_entries) == 104); + assert!(core::mem::offset_of!(RawSystemTable, configuration_table) == 112); +}; + +/// `EFI_CONFIGURATION_TABLE` (UEFI 2.10 §4.6). +#[repr(C)] +pub struct ConfigurationTable { + pub guid: Guid, + pub address: *const c_void, +} + +const _: () = assert!(size_of::() == 24); +const _: () = assert!(core::mem::offset_of!(ConfigurationTable, address) == 16); + +/// `EFI_ACPI_20_TABLE_GUID` (UEFI 2.10 §4.6.1). +pub const ACPI2_GUID: Guid = Guid::new(0x8868e871, 0xe4f1, 0x11d3, [0xbc, 0x22, 0x00, 0x80, 0xc7, 0x3c, 0x88, 0x81]); + +/// The system table firmware handed `efi_main`, while boot services live: +/// null before the entry and from [`SystemTable::exit_boot_services`] on. The +/// console and the allocator read it; nothing else does. +static SYSTEM: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + +/// Set before the first `ExitBootServices` call: from then on only the memory +/// allocation services may be called, even where the call fails (UEFI 2.10 +/// §7.4.6), so firmware's console is not written again. +static EXITING: AtomicBool = AtomicBool::new(false); + +/// This image's handle, the agent every protocol open names. +static IMAGE: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + +/// The system table while boot services live: made once, by the entry, and +/// consumed by [`SystemTable::exit_boot_services`]. +pub struct SystemTable { + raw: &'static RawSystemTable, +} + +impl SystemTable { + pub fn boot_services(&self) -> &BootServices { + // SAFETY: firmware's table names its boot services for as long as they + // live, which is as long as `self`. + unsafe { &*self.raw.boot_services } + } + + pub fn runtime_services(&self) -> &RuntimeServices { + // SAFETY: as `boot_services`; runtime services outlive them. + unsafe { &*self.raw.runtime_services } + } + + pub fn config_table(&self) -> &[ConfigurationTable] { + // SAFETY: firmware's table names this many entries at this address. + unsafe { core::slice::from_raw_parts(self.raw.configuration_table, self.raw.number_of_table_entries) } + } + + /// `ClearScreen` (UEFI 2.10 §12.4.8), which also homes the cursor. + pub fn clear_screen(&self) -> Result<(), Status> { + let out = self.raw.con_out; + // SAFETY: `ConOut` is firmware's console for as long as boot services live. + unsafe { ((*out).clear_screen)(out) }.ok() + } + + /// `ExitBootServices` (UEFI 2.10 §7.4.6) on the map it is handed, taken + /// into pool memory it never frees; tried twice, as a map key gone stale + /// between the two calls is answered by asking again, and a machine that + /// refuses twice is reset. The console and the allocator refuse from here. + pub fn exit_boot_services(self) -> MemoryMap<'static> { + let bs = self.boot_services(); + let reset = |status: Status| -> ! { self.runtime_services().reset(ResetType::COLD, status) }; + // Eight descriptors past the measured map, for the ones the pool + // allocation below adds. + let (map_size, entry_size) = bs.memory_map_size(); + let Some(bytes) = entry_size.checked_mul(8).and_then(|extra| map_size.checked_add(extra)) else { + reset(Status::ABORTED) + }; + let words = bytes.div_ceil(size_of::()); + let buffer = match bs.allocate_pool(words * size_of::()) { + // SAFETY: the pool gives 8-byte-aligned memory (§7.2.4) of the + // size asked, never freed: it is this map's for good. + Ok(at) => unsafe { core::slice::from_raw_parts_mut(at.cast::(), words) }, + Err(status) => reset(status), + }; + let image = image(); + let mut status = Status::ABORTED; + for _ in 0..2 { + let filled = match bs.fill_memory_map(buffer) { + Ok(filled) => filled, + Err(why) => { + status = why; + continue; + } + }; + EXITING.store(true, Ordering::Release); + status = bs.exit(image, filled.key); + if status.is_success() { + SYSTEM.store(ptr::null_mut(), Ordering::Release); + return MemoryMap::new(buffer, filled); + } + } + reset(status) + } +} + +fn image() -> Handle { + Handle::from_ptr(IMAGE.load(Ordering::Acquire)).expect("the entry stored this image's handle") +} + +/// The system table while boot services live, for the two readers that run +/// without one in hand. +fn live() -> Option<&'static RawSystemTable> { + // SAFETY: `SYSTEM` is null or the table firmware handed the entry, whose + // boot services are live until `exit_boot_services` nulls it. + unsafe { SYSTEM.load(Ordering::Acquire).as_ref() } +} + +/// The system table while firmware's console may be written: until the first +/// `ExitBootServices` call. +fn console() -> Option<&'static RawSystemTable> { + live().filter(|_| !EXITING.load(Ordering::Acquire)) +} + +/// The image's entry (UEFI 2.10 §4.1, `EFI_IMAGE_ENTRY_POINT`), under the name +/// the UEFI targets link as the PE entry point. +#[unsafe(no_mangle)] +extern "efiapi" fn efi_main(image: *mut c_void, system_table: *mut RawSystemTable) -> Status { + // SAFETY: firmware hands its system table and this image's handle, live + // until `ExitBootServices`. + let raw = unsafe { system_table.as_ref() }.expect("firmware hands the entry its system table"); + let image = Handle::from_ptr(image).expect("firmware hands the entry this image's handle"); + IMAGE.store(image.as_ptr(), Ordering::Release); + SYSTEM.store(system_table, Ordering::Release); + crate::main(image, SystemTable { raw }) +} + +/// One line to firmware's console, `\n` as `\r\n`, in `CHAR16` chunks; a +/// character past UCS-2 is written as U+FFFD. +/// +/// # Panics +/// From the first `ExitBootServices` call, and where the console refuses the +/// line. +pub fn print(args: fmt::Arguments) { + let raw = console().expect("the console is gone with boot services"); + let mut out = Console { out: raw.con_out, units: [0; Console::CHUNK + 1], len: 0 }; + let written = fmt::write(&mut out, args).and_then(|()| out.flush()); + assert!(written.is_ok(), "firmware's console refused a line"); +} + +struct Console { + out: *mut proto::TextOutput, + units: [u16; Console::CHUNK + 1], + len: usize, +} + +impl Console { + const CHUNK: usize = 128; + + fn push(&mut self, unit: u16) -> fmt::Result { + self.units[self.len] = unit; + self.len += 1; + if self.len == Self::CHUNK { self.flush() } else { Ok(()) } + } + + fn flush(&mut self) -> fmt::Result { + if self.len == 0 { + return Ok(()); + } + self.units[self.len] = 0; + self.len = 0; + // SAFETY: `ConOut` is live while `SYSTEM` is set, and `units` is + // NUL-terminated at the count just written. + let status = unsafe { ((*self.out).output_string)(self.out, self.units.as_ptr()) }; + status.ok().map_err(|_| fmt::Error) + } +} + +impl fmt::Write for Console { + fn write_str(&mut self, s: &str) -> fmt::Result { + for ch in s.chars() { + if ch == '\n' { + self.push(u16::from(b'\r'))?; + } + self.push(u16::try_from(u32::from(ch)).unwrap_or(0xFFFD))?; + } + Ok(()) + } +} + +/// Rust's heap, in `EfiLoaderData` pool memory (UEFI 2.10 §7.2.4: a pool +/// allocation is 8-byte aligned; an application's data is `EfiLoaderData`, +/// §7.4.1), and null once boot services are gone. A wider alignment is cut +/// out of a larger allocation, the allocation's own address kept in the word +/// before it. +struct Pool; + +#[global_allocator] +static POOL: Pool = Pool; + +/// What `AllocatePool` aligns to. +const POOL_ALIGN: usize = 8; + +// SAFETY: every pointer handed out is pool memory of at least the layout's +// size at its alignment, and is given back only through `dealloc`. +unsafe impl core::alloc::GlobalAlloc for Pool { + unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 { + let Some(raw) = live() else { return ptr::null_mut() }; + // SAFETY: live boot services. + let bs = unsafe { &*raw.boot_services }; + if layout.align() <= POOL_ALIGN { + return bs.allocate_pool(layout.size()).unwrap_or(ptr::null_mut()); + } + let Some(size) = layout.size().checked_add(layout.align()) else { return ptr::null_mut() }; + let Ok(whole) = bs.allocate_pool(size) else { return ptr::null_mut() }; + // At least one word ahead of the aligned address, for the allocation's own. + let offset = match whole.align_offset(layout.align()) { + 0 => layout.align(), + n => n, + }; + // SAFETY: `offset <= align`, inside the `size + align` bytes; the + // word before the aligned address is inside them too, as + // `offset >= POOL_ALIGN`, and aligned, as both addresses are. + unsafe { + let aligned = whole.add(offset); + aligned.cast::<*mut u8>().sub(1).write(whole); + aligned + } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: core::alloc::Layout) { + let raw = live().expect("nothing is freed once boot services are gone"); + let whole = if layout.align() <= POOL_ALIGN { + ptr + } else { + // SAFETY: `alloc` wrote the allocation's address in the word before. + unsafe { ptr.cast::<*mut u8>().sub(1).read() } + }; + // SAFETY: live boot services, and `whole` is a pool allocation of theirs. + let freed = unsafe { (*raw.boot_services).free_pool(whole) }; + assert!(freed.is_ok(), "firmware would not take back pool memory it gave"); + } +} + +/// Set by the first panic, so a panic while saying one goes straight to the +/// power-off. +static PANICKED: AtomicBool = AtomicBool::new(false); + +/// Say the panic on firmware's console and power the machine off; from the +/// first `ExitBootServices` call the console is not written, and once boot +/// services are gone neither is there, and the CPU spins. +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + if let Some(raw) = live() { + if console().is_some() && !PANICKED.swap(true, Ordering::Relaxed) { + print(format_args!("[PANIC]: {info}\n")); + } + // SAFETY: runtime services live as long as the table. + unsafe { &*raw.runtime_services }.reset(ResetType::SHUTDOWN, Status::ABORTED) + } + loop { + core::hint::spin_loop(); + } +} diff --git a/bootloader/src/efi/proto.rs b/bootloader/src/efi/proto.rs new file mode 100644 index 00000000000..c33d49b2f85 --- /dev/null +++ b/bootloader/src/efi/proto.rs @@ -0,0 +1,663 @@ +//! The protocols this loader opens, each in its spec section's own field +//! order, and the console it writes through. + +use core::ffi::c_void; +use core::mem::offset_of; +use core::ptr::{self, NonNull}; + +use alloc::vec; + +use super::boot::{Exclusive, Protocol}; +use super::{CStr16, Guid, Handle, Status}; + +type Unused = *const c_void; + +/// `EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL` (UEFI 2.10 §12.4.1), firmware's console: +/// reached through the system table's `ConOut`, never opened. +#[repr(C)] +pub(super) struct TextOutput { + reset: Unused, + pub(super) output_string: unsafe extern "efiapi" fn(this: *mut TextOutput, string: *const u16) -> Status, + test_string: Unused, + query_mode: Unused, + set_mode: Unused, + set_attribute: Unused, + pub(super) clear_screen: unsafe extern "efiapi" fn(this: *mut TextOutput) -> Status, + set_cursor_position: Unused, + enable_cursor: Unused, + mode: Unused, +} + +const _: () = { + assert!(size_of::() == 80); + assert!(offset_of!(TextOutput, output_string) == 8); + assert!(offset_of!(TextOutput, clear_screen) == 48); +}; + +/// `EFI_LOADED_IMAGE_PROTOCOL` (UEFI 2.10 §9.1.1). +#[repr(C)] +pub struct LoadedImage { + revision: u32, + parent_handle: *mut c_void, + system_table: Unused, + device_handle: *mut c_void, + file_path: Unused, + reserved: Unused, + load_options_size: u32, + load_options: Unused, + image_base: *const c_void, + image_size: u64, + image_code_type: u32, + image_data_type: u32, + unload: Unused, +} + +const _: () = { + assert!(size_of::() == 96); + assert!(offset_of!(LoadedImage, device_handle) == 24); + assert!(offset_of!(LoadedImage, load_options_size) == 48); + assert!(offset_of!(LoadedImage, image_base) == 64); + assert!(offset_of!(LoadedImage, image_size) == 72); + assert!(offset_of!(LoadedImage, image_data_type) == 84); +}; + +// SAFETY: §9.1.1's layout, under its GUID. +unsafe impl Protocol for LoadedImage { + const GUID: Guid = Guid::new(0x5b1b31a1, 0x9562, 0x11d2, [0x8e, 0x3f, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} +impl Exclusive for LoadedImage {} + +impl LoadedImage { + /// The handle of the device firmware loaded this image from, if it says. + pub fn device(&self) -> Option { + Handle::from_ptr(self.device_handle) + } + + /// Where firmware loaded the image, and its size in bytes. + pub fn info(&self) -> (*const c_void, u64) { + (self.image_base, self.image_size) + } +} + +/// `EFI_DEVICE_PATH_PROTOCOL` (UEFI 2.10 §10.2): the first node's header, +/// which the rest of the path follows. +#[repr(C)] +pub struct DevicePath { + ty: u8, + sub_type: u8, + length: [u8; 2], +} + +const _: () = assert!(size_of::() == 4); + +// SAFETY: §10.2's layout, under its GUID. +unsafe impl Protocol for DevicePath { + const GUID: Guid = Guid::new(0x09576e91, 0x6d3f, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} +impl Exclusive for DevicePath {} + +/// A device path node's header: type, subtype, and a length that counts it. +const NODE_HEADER: usize = 4; + +/// End of Entire Device Path (§10.3.1). +const END_ENTIRE: (u8, u8) = (0x7f, 0xff); + +impl DevicePath { + /// Each node, whole, up to the End of Entire node. + /// + /// # Panics + /// Where a node's length does not cover its own header: the walk could + /// not step past it. + pub fn nodes(&self) -> impl Iterator { + let mut at = ptr::from_ref(self).cast::(); + core::iter::from_fn(move || { + // SAFETY: firmware's path is a run of nodes ending in End of + // Entire, and `at` is the start of one of them. + let header = unsafe { core::slice::from_raw_parts(at, NODE_HEADER) }; + if (header[0], header[1]) == END_ENTIRE { + return None; + } + let len = usize::from(u16::from_le_bytes([header[2], header[3]])); + assert!(len >= NODE_HEADER, "firmware's device path has a {len}-byte node"); + // SAFETY: the node's own length, which counts its header. + let node = unsafe { core::slice::from_raw_parts(at, len) }; + // SAFETY: the next node follows this one. + at = unsafe { at.add(len) }; + Some(node) + }) + } +} + +/// The MEDIA/HARDDRIVE node (§10.3.5.1). +pub struct HardDrive { + pub start: u64, + pub size: u64, + pub signature: [u8; 16], + /// `MBRType`: 2 for a GPT partition. + pub format: u8, + /// `SignatureType`: 2 for a GUID signature. + pub signature_type: u8, +} + +impl HardDrive { + pub const TYPE: (u8, u8) = (4, 1); + /// `MBRType` of a GPT partition. + pub const GPT: u8 = 2; + /// `SignatureType` of a GUID signature. + pub const GUID_SIGNATURE: u8 = 2; + const LEN: usize = 42; + + /// `node`, where it is a HARDDRIVE node of the length §10.3.5.1 gives it: + /// a node of a device path firmware built. A load option's path is any + /// writer's bytes, and `bootnext` walks it by its own rule. + pub fn parse(node: &[u8]) -> Option { + if node.len() != Self::LEN || (node[0], node[1]) != Self::TYPE { + return None; + } + let u64_at = |at: usize| u64::from_le_bytes(node[at..at + 8].try_into().expect("eight bytes")); + Some(HardDrive { + start: u64_at(8), + size: u64_at(16), + signature: node[24..40].try_into().expect("sixteen bytes"), + format: node[40], + signature_type: node[41], + }) + } +} + +/// `EFI_SIMPLE_FILE_SYSTEM_PROTOCOL` (UEFI 2.10 §13.4.1). +#[repr(C)] +pub struct SimpleFileSystem { + revision: u64, + open_volume: unsafe extern "efiapi" fn(this: *mut SimpleFileSystem, root: *mut *mut FileProtocol) -> Status, +} + +const _: () = assert!(size_of::() == 16); + +// SAFETY: §13.4.1's layout, under its GUID. +unsafe impl Protocol for SimpleFileSystem { + const GUID: Guid = Guid::new(0x964e5b22, 0x6459, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} +impl Exclusive for SimpleFileSystem {} + +impl super::Scoped<'_, SimpleFileSystem> { + /// `OpenVolume` (§13.4.2): the volume's root directory. + pub fn open_volume(&mut self) -> Result { + let mut root = ptr::null_mut(); + // SAFETY: the open interface, and a live out parameter. + unsafe { (self.open_volume)(self.this(), &mut root) }.ok()?; + Ok(File(NonNull::new(root).expect("OpenVolume answered success with no root"))) + } +} + +/// `EFI_FILE_PROTOCOL` (UEFI 2.10 §13.5.1), through `Flush`: the revision 2 +/// functions after it are never called. +#[repr(C)] +pub struct FileProtocol { + revision: u64, + open: unsafe extern "efiapi" fn( + this: *mut FileProtocol, + new: *mut *mut FileProtocol, + name: *const u16, + mode: u64, + attributes: u64, + ) -> Status, + close: unsafe extern "efiapi" fn(this: *mut FileProtocol) -> Status, + delete: unsafe extern "efiapi" fn(this: *mut FileProtocol) -> Status, + read: unsafe extern "efiapi" fn(this: *mut FileProtocol, size: *mut usize, buffer: *mut u8) -> Status, + write: unsafe extern "efiapi" fn(this: *mut FileProtocol, size: *mut usize, buffer: *const u8) -> Status, + get_position: Unused, + set_position: unsafe extern "efiapi" fn(this: *mut FileProtocol, position: u64) -> Status, + get_info: unsafe extern "efiapi" fn( + this: *mut FileProtocol, + kind: *const Guid, + size: *mut usize, + buffer: *mut u8, + ) -> Status, + set_info: Unused, + flush: unsafe extern "efiapi" fn(this: *mut FileProtocol) -> Status, +} + +const _: () = { + assert!(size_of::() == 88); + assert!(offset_of!(FileProtocol, open) == 8); + assert!(offset_of!(FileProtocol, close) == 16); + assert!(offset_of!(FileProtocol, delete) == 24); + assert!(offset_of!(FileProtocol, read) == 32); + assert!(offset_of!(FileProtocol, write) == 40); + assert!(offset_of!(FileProtocol, set_position) == 56); + assert!(offset_of!(FileProtocol, get_info) == 64); + assert!(offset_of!(FileProtocol, flush) == 80); +}; + +/// `Open`'s modes (§13.5.2). +#[derive(Clone, Copy)] +pub enum Mode { + Read, + ReadWrite, + CreateReadWrite, +} + +impl Mode { + fn bits(self) -> u64 { + const READ: u64 = 0x1; + const WRITE: u64 = 0x2; + const CREATE: u64 = 0x8000_0000_0000_0000; + match self { + Mode::Read => READ, + Mode::ReadWrite => READ | WRITE, + Mode::CreateReadWrite => CREATE | READ | WRITE, + } + } +} + +/// An open file or directory, closed when this drops. +pub struct File(NonNull); + +/// `EFI_FILE_INFO` (§13.5.16), up to the name this loader never reads. +#[repr(C)] +struct RawFileInfo { + size: u64, + file_size: u64, + physical_size: u64, + create_time: super::Time, + last_access_time: super::Time, + modification_time: super::Time, + attribute: u64, +} + +const _: () = { + assert!(size_of::() == 80); + assert!(offset_of!(RawFileInfo, file_size) == 8); + assert!(offset_of!(RawFileInfo, attribute) == 72); +}; + +/// `EFI_FILE_INFO_ID` (§13.5.16). +const FILE_INFO: Guid = Guid::new(0x09576e92, 0x6d3f, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); + +/// `EFI_FILE_DIRECTORY` (§13.5.16). +const DIRECTORY: u64 = 0x10; + +/// What this loader reads of a file's `EFI_FILE_INFO`. +pub struct FileInfo { + pub file_size: u64, + pub directory: bool, +} + +impl File { + fn call(&mut self, f: impl FnOnce(&FileProtocol, *mut FileProtocol) -> R) -> R { + // SAFETY: an open handle's interface, live until it is closed. + f(unsafe { self.0.as_ref() }, self.0.as_ptr()) + } + + /// `Open` (§13.5.2), relative to this directory. + pub fn open(&mut self, name: &CStr16, mode: Mode) -> Result { + let mut new = ptr::null_mut(); + // SAFETY: a NUL-terminated name and a live out parameter. + self.call(|f, this| unsafe { (f.open)(this, &mut new, name.as_ptr(), mode.bits(), 0) }).ok()?; + Ok(File(NonNull::new(new).expect("Open answered success with no handle"))) + } + + /// `Delete` (§13.5.4), which closes the handle whatever it answers. + pub fn delete(mut self) -> Result<(), Status> { + // SAFETY: the handle, which `Delete` closes, so `Drop` must not. + let status = self.call(|f, this| unsafe { (f.delete)(this) }); + #[expect(clippy::disallowed_methods, reason = "`Delete` closed the handle `Drop` would close again")] + core::mem::forget(self); + status.ok() + } + + /// `Read` (§13.5.5): the bytes read, at most `buffer`'s length. + pub fn read(&mut self, buffer: &mut [u8]) -> Result { + let mut size = buffer.len(); + // SAFETY: `buffer` is `size` writable bytes. + self.call(|f, this| unsafe { (f.read)(this, &mut size, buffer.as_mut_ptr()) }).ok()?; + assert!(size <= buffer.len(), "Read answered {size} bytes into {}", buffer.len()); + Ok(size) + } + + /// `Write` (§13.5.6): on a refusal, the status and the bytes written + /// before it. + pub fn write(&mut self, buffer: &[u8]) -> Result<(), (Status, usize)> { + let mut size = buffer.len(); + // SAFETY: `buffer` is `size` readable bytes. + self.call(|f, this| unsafe { (f.write)(this, &mut size, buffer.as_ptr()) }).ok().map_err(|status| (status, size)) + } + + /// `SetPosition` (§13.5.8); `u64::MAX` is the end of the file. + pub fn set_position(&mut self, position: u64) -> Result<(), Status> { + // SAFETY: a plain call on the handle. + self.call(|f, this| unsafe { (f.set_position)(this, position) }).ok() + } + + /// `Flush` (§13.5.11). + pub fn flush(&mut self) -> Result<(), Status> { + // SAFETY: a plain call on the handle. + self.call(|f, this| unsafe { (f.flush)(this) }).ok() + } + + /// `GetInfo` (§13.5.12) of `EFI_FILE_INFO`, sized by asking first. + pub fn info(&mut self) -> Result { + let mut size = 0usize; + // SAFETY: a zero size with no buffer asks for the size. + let status = self.call(|f, this| unsafe { (f.get_info)(this, &FILE_INFO, &mut size, ptr::null_mut()) }); + if status != Status::BUFFER_TOO_SMALL { + return Err(if status.is_success() { Status::BUFFER_TOO_SMALL } else { status }); + } + assert!(size >= size_of::(), "GetInfo wants {size} bytes for an EFI_FILE_INFO"); + let mut words = vec![0u64; size.div_ceil(size_of::())]; + // SAFETY: `words` is at least `size` writable bytes, aligned for the info. + self.call(|f, this| unsafe { (f.get_info)(this, &FILE_INFO, &mut size, words.as_mut_ptr().cast()) }).ok()?; + // SAFETY: firmware wrote an `EFI_FILE_INFO`, whose head is this. + let info = unsafe { words.as_ptr().cast::().read() }; + Ok(FileInfo { file_size: info.file_size, directory: info.attribute & DIRECTORY != 0 }) + } + + /// This handle, where it is a file and not a directory; `None` for a + /// directory, and for a handle that would not say which it is. + pub fn into_regular_file(mut self) -> Option { + match self.info() { + Ok(FileInfo { directory: false, .. }) => Some(self), + _ => None, + } + } +} + +impl Drop for File { + fn drop(&mut self) { + // SAFETY: the handle, closed once. `Close` always succeeds (§13.5.3). + let _ = self.call(|f, this| unsafe { (f.close)(this) }); + } +} + +/// `EFI_PARTITION_INFO_PROTOCOL` (UEFI 2.10 §13.18), packed as the spec +/// declares it; the record is MBR or GPT by `kind`, read as bytes. +#[repr(C, packed)] +pub struct PartitionInfo { + revision: u32, + kind: u32, + system: u8, + reserved: [u8; 7], + record: [u8; 128], +} + +const _: () = { + assert!(size_of::() == 144); + assert!(offset_of!(PartitionInfo, kind) == 4); + assert!(offset_of!(PartitionInfo, system) == 8); + assert!(offset_of!(PartitionInfo, record) == 16); +}; + +// SAFETY: §13.18's layout, under its GUID. +unsafe impl Protocol for PartitionInfo { + const GUID: Guid = Guid::new(0x8cf2f62c, 0xbc9b, 0x4821, [0x80, 0x8d, 0xec, 0x9e, 0xc4, 0x21, 0xa1, 0xa0]); +} + +impl PartitionInfo { + /// `EFI_PARTITION_INFO_PROTOCOL_REVISION`, which the spec spells `0x0001000`. + const REVISION: u32 = 0x1000; + /// `PARTITION_TYPE_GPT`. + const GPT: u32 = 0x02; + + /// The unique partition GUID of the GPT entry this carries (UEFI 2.10 + /// §5.3.3, bytes 16 to 32 of the entry), or `None` for any other record. + pub fn gpt_unique_guid(&self) -> Option<[u8; 16]> { + let (revision, kind) = (self.revision, self.kind); + if revision != Self::REVISION || kind != Self::GPT { + return None; + } + let record = self.record; + Some(record[16..32].try_into().expect("sixteen bytes")) + } +} + +/// `EFI_BLOCK_IO_PROTOCOL` (UEFI 2.10 §13.9.1). +#[repr(C)] +pub struct BlockIo { + pub revision: u64, + media: *const BlockIoMedia, + reset: Unused, + read_blocks: unsafe extern "efiapi" fn( + this: *mut BlockIo, + media_id: u32, + lba: u64, + size: usize, + buffer: *mut u8, + ) -> Status, + write_blocks: Unused, + flush_blocks: Unused, +} + +const _: () = { + assert!(size_of::() == 48); + assert!(offset_of!(BlockIo, media) == 8); + assert!(offset_of!(BlockIo, read_blocks) == 24); +}; + +// SAFETY: §13.9.1's layout, under its GUID. +unsafe impl Protocol for BlockIo { + const GUID: Guid = Guid::new(0x964e5b21, 0x6459, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} + +/// `EFI_BLOCK_IO_MEDIA` (§13.9.1); `optimal_transfer_length_granularity` is +/// there only from `EFI_BLOCK_IO_PROTOCOL_REVISION3` on. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct BlockIoMedia { + pub media_id: u32, + removable_media: u8, + media_present: u8, + logical_partition: u8, + read_only: u8, + write_caching: u8, + pub block_size: u32, + pub io_align: u32, + pub last_block: u64, +} + +/// The revision 2 and 3 fields after `LastBlock`. +#[repr(C)] +struct BlockIoMediaRevision3 { + head: BlockIoMedia, + lowest_aligned_lba: u64, + logical_blocks_per_physical_block: u32, + optimal_transfer_length_granularity: u32, +} + +const _: () = { + assert!(size_of::() == 32); + assert!(offset_of!(BlockIoMedia, media_present) == 5); + assert!(offset_of!(BlockIoMedia, block_size) == 12); + assert!(offset_of!(BlockIoMedia, io_align) == 16); + assert!(offset_of!(BlockIoMedia, last_block) == 24); + assert!(size_of::() == 48); + assert!(offset_of!(BlockIoMediaRevision3, optimal_transfer_length_granularity) == 44); +}; + +impl BlockIoMedia { + pub fn is_media_present(&self) -> bool { + self.media_present != 0 + } +} + +impl BlockIo { + /// The first revision whose media carries `OptimalTransferLengthGranularity`. + pub const REVISION3: u64 = 0x0002_001f; + + /// The media as firmware describes it now, through `LastBlock`. + pub fn media(&self) -> BlockIoMedia { + // SAFETY: every revision's media begins with this head. + unsafe { self.media.read() } + } + + /// `OptimalTransferLengthGranularity`, where the revision carries it. + pub fn optimal_transfer_length_granularity(&self) -> Option { + if self.revision < Self::REVISION3 { + return None; + } + // SAFETY: a revision 3 media carries the field. + Some(unsafe { (*self.media.cast::()).optimal_transfer_length_granularity }) + } +} + +impl super::Scoped<'_, BlockIo> { + /// `ReadBlocks` (§13.9.3): `buffer`'s length in blocks from `lba`. + pub fn read_blocks(&self, media_id: u32, lba: u64, buffer: &mut [u8]) -> Result<(), Status> { + // SAFETY: the open interface, and `buffer` is its length of writable bytes. + unsafe { (self.read_blocks)(self.this(), media_id, lba, buffer.len(), buffer.as_mut_ptr()) }.ok() + } +} + +/// `EFI_GRAPHICS_OUTPUT_PROTOCOL` (UEFI 2.10 §12.9.2). +#[repr(C)] +pub struct Gop { + query_mode: Unused, + set_mode: Unused, + blt: Unused, + mode: *const GopMode, +} + +/// `EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE` (§12.9.2). +#[repr(C)] +struct GopMode { + max_mode: u32, + mode: u32, + info: *const GopModeInfo, + size_of_info: usize, + frame_buffer_base: u64, + frame_buffer_size: usize, +} + +/// `EFI_GRAPHICS_OUTPUT_MODE_INFORMATION` (§12.9.2). +#[repr(C)] +pub struct GopModeInfo { + version: u32, + pub horizontal_resolution: u32, + pub vertical_resolution: u32, + pub pixel_format: PixelFormat, + pixel_information: [u32; 4], + pub pixels_per_scan_line: u32, +} + +const _: () = { + assert!(size_of::() == 32); + assert!(offset_of!(Gop, mode) == 24); + assert!(size_of::() == 40); + assert!(offset_of!(GopMode, info) == 8); + assert!(offset_of!(GopMode, frame_buffer_base) == 24); + assert!(offset_of!(GopMode, frame_buffer_size) == 32); + assert!(size_of::() == 36); + assert!(offset_of!(GopModeInfo, pixel_format) == 12); + assert!(offset_of!(GopModeInfo, pixels_per_scan_line) == 32); +}; + +/// `EFI_GRAPHICS_PIXEL_FORMAT` (§12.9.2), as the raw word firmware wrote. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct PixelFormat(pub u32); + +impl PixelFormat { + pub const RGB: PixelFormat = PixelFormat(0); + pub const BGR: PixelFormat = PixelFormat(1); + pub const BIT_MASK: PixelFormat = PixelFormat(2); + pub const BLT_ONLY: PixelFormat = PixelFormat(3); +} + +impl core::fmt::Debug for PixelFormat { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match *self { + Self::RGB => f.write_str("PixelRedGreenBlueReserved8BitPerColor"), + Self::BGR => f.write_str("PixelBlueGreenRedReserved8BitPerColor"), + Self::BIT_MASK => f.write_str("PixelBitMask"), + Self::BLT_ONLY => f.write_str("PixelBltOnly"), + PixelFormat(other) => write!(f, "PixelFormat({other})"), + } + } +} + +// SAFETY: §12.9.2's layout, under its GUID. +unsafe impl Protocol for Gop { + const GUID: Guid = Guid::new(0x9042a9de, 0x23dc, 0x4a38, [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a]); +} + +impl Gop { + /// The mode firmware set. + pub fn current_mode_info(&self) -> &GopModeInfo { + // SAFETY: an open GOP names its mode and the mode its information. + unsafe { &*(*self.mode).info } + } + + /// The frame buffer's physical base and size in bytes. + pub fn frame_buffer(&self) -> (u64, u64) { + // SAFETY: an open GOP names its mode. + let mode = unsafe { &*self.mode }; + (mode.frame_buffer_base, mode.frame_buffer_size as u64) + } +} + +/// `EFI_RNG_PROTOCOL` (UEFI 2.10 §37.5.1). +#[repr(C)] +pub struct Rng { + get_info: Unused, + get_rng: unsafe extern "efiapi" fn(this: *mut Rng, algorithm: *const Guid, size: usize, value: *mut u8) -> Status, +} + +const _: () = assert!(size_of::() == 16); + +// SAFETY: §37.5.1's layout, under its GUID. +unsafe impl Protocol for Rng { + const GUID: Guid = Guid::new(0x3152bca5, 0xeade, 0x433d, [0x86, 0x2e, 0xc0, 0x1c, 0xdc, 0x29, 0x1f, 0x44]); +} + +impl super::Scoped<'_, Rng> { + /// `GetRNG` (§37.5.3) with no algorithm named: firmware's default. + pub fn get_rng(&self, into: &mut [u8]) -> Result<(), Status> { + // SAFETY: the open interface, and `into` is its length of writable bytes. + unsafe { (self.get_rng)(self.this(), ptr::null(), into.len(), into.as_mut_ptr()) }.ok() + } +} + +/// `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL` (UEFI 2.10 §14.2.1). +#[repr(C)] +pub struct PciRootBridgeIo { + parent_handle: Unused, + poll_mem: Unused, + poll_io: Unused, + mem_read: Unused, + mem_write: Unused, + io_read: Unused, + io_write: Unused, + pci_read: Unused, + pci_write: Unused, + copy_mem: Unused, + map: Unused, + unmap: Unused, + allocate_buffer: Unused, + free_buffer: Unused, + flush: Unused, + get_attributes: Unused, + set_attributes: Unused, + configuration: unsafe extern "efiapi" fn(this: *mut PciRootBridgeIo, resources: *mut *const c_void) -> Status, + pub segment_number: u32, +} + +const _: () = { + assert!(size_of::() == 18 * 8 + 8); + assert!(offset_of!(PciRootBridgeIo, configuration) == 17 * 8); + assert!(offset_of!(PciRootBridgeIo, segment_number) == 18 * 8); +}; + +// SAFETY: §14.2.1's layout, under its GUID. +unsafe impl Protocol for PciRootBridgeIo { + const GUID: Guid = Guid::new(0x2f707ebb, 0x4a1a, 0x11d4, [0x9a, 0x38, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d]); +} + +impl super::Scoped<'_, PciRootBridgeIo> { + /// `Configuration()` (§14.2.13): the bridge's ACPI resource descriptors, + /// in memory firmware owns. + pub fn configuration(&self) -> Result<*const c_void, Status> { + let mut resources = ptr::null(); + // SAFETY: the open interface, and a live out parameter. + unsafe { (self.configuration)(self.this(), &mut resources) }.ok().map(|()| resources) + } +} diff --git a/bootloader/src/efi/runtime.rs b/bootloader/src/efi/runtime.rs new file mode 100644 index 00000000000..16bca2e3487 --- /dev/null +++ b/bootloader/src/efi/runtime.rs @@ -0,0 +1,175 @@ +//! `EFI_RUNTIME_SERVICES` (UEFI 2.10 §4.5) and what this loader calls of +//! them: the time, variables, and the reset. + +use core::ffi::c_void; +use core::mem::{offset_of, MaybeUninit}; +use core::ptr; + +use alloc::vec; +use alloc::vec::Vec; + +use super::{CStr16, CString16, Guid, Status, TableHeader}; + +type Unused = *const c_void; + +/// `EFI_RUNTIME_SERVICES` (§4.5), in the spec's own field order. +#[repr(C)] +pub struct RuntimeServices { + hdr: TableHeader, + get_time: unsafe extern "efiapi" fn(time: *mut Time, capabilities: *mut c_void) -> Status, + set_time: Unused, + get_wakeup_time: Unused, + set_wakeup_time: Unused, + set_virtual_address_map: Unused, + convert_pointer: Unused, + get_variable: unsafe extern "efiapi" fn( + name: *const u16, + vendor: *const Guid, + attributes: *mut u32, + size: *mut usize, + data: *mut u8, + ) -> Status, + get_next_variable_name: unsafe extern "efiapi" fn(size: *mut usize, name: *mut u16, vendor: *mut Guid) -> Status, + set_variable: unsafe extern "efiapi" fn( + name: *const u16, + vendor: *const Guid, + attributes: u32, + size: usize, + data: *const u8, + ) -> Status, + get_next_high_monotonic_count: Unused, + reset_system: unsafe extern "efiapi" fn(kind: ResetType, status: Status, size: usize, data: *const c_void) -> !, + update_capsule: Unused, + query_capsule_capabilities: Unused, + query_variable_info: Unused, +} + +const _: () = { + assert!(size_of::() == 24 + 14 * 8); + assert!(offset_of!(RuntimeServices, get_time) == 24); + assert!(offset_of!(RuntimeServices, get_variable) == 72); + assert!(offset_of!(RuntimeServices, get_next_variable_name) == 80); + assert!(offset_of!(RuntimeServices, set_variable) == 88); + assert!(offset_of!(RuntimeServices, reset_system) == 104); +}; + +/// `EFI_TIME` (§8.3). +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Time { + pub year: u16, + pub month: u8, + pub day: u8, + pub hour: u8, + pub minute: u8, + pub second: u8, + pad1: u8, + pub nanosecond: u32, + pub time_zone: i16, + pub daylight: u8, + pad2: u8, +} + +const _: () = { + assert!(size_of::

(handle) -} - -#[allow(clippy::disallowed_methods, reason = "the one attribute it passes stops no driver")] -pub fn get(bs: &BootServices, handle: Handle) -> uefi::Result> { - // SAFETY: `open_protocol`'s obligation is that the handle and its protocol - // stay installed until the `ScopedProtocol` drops. Nothing between the two - // can uninstall either: the loader is the one image running, it registers - // no event callback, and it calls no boot service that connects or - // disconnects a controller. - unsafe { - bs.open_protocol::

( - OpenProtocolParams { handle, agent: bs.image_handle(), controller: None }, - OpenProtocolAttributes::GetProtocol, - ) - } -} diff --git a/bootloader/src/rootbridge.rs b/bootloader/src/rootbridge.rs index c1682f0d55b..f6d3060e8d9 100644 --- a/bootloader/src/rootbridge.rs +++ b/bootloader/src/rootbridge.rs @@ -12,47 +12,14 @@ //! the reason [`toyos_abi::boot::KernelArgs::root_bridge_windows`] states: a //! list missing one window is worse than no list. -use core::ffi::c_void; use core::ptr::read_volatile; use toyos_abi::boot::RootBridgeWindow; use toyos_acpi::{memory_windows, Phys, MAX_LIST_BYTES}; -use uefi::prelude::*; -use uefi::proto::unsafe_protocol; +use crate::efi::{PciRootBridgeIo, Status, SystemTable}; const HEAD: &str = "Root bridge:"; -/// UEFI 2.10 §14.2.2, in the spec's own field order. -#[repr(C)] -#[unsafe_protocol("2f707ebb-4a1a-11d4-9a38-0090273fc14d")] -struct PciRootBridgeIo { - parent_handle: *mut c_void, - poll_mem: *mut c_void, - poll_io: *mut c_void, - mem_read: *mut c_void, - mem_write: *mut c_void, - io_read: *mut c_void, - io_write: *mut c_void, - pci_read: *mut c_void, - pci_write: *mut c_void, - copy_mem: *mut c_void, - map: *mut c_void, - unmap: *mut c_void, - allocate_buffer: *mut c_void, - free_buffer: *mut c_void, - flush: *mut c_void, - get_attributes: *mut c_void, - set_attributes: *mut c_void, - configuration: - unsafe extern "efiapi" fn(this: *const PciRootBridgeIo, resources: *mut *const c_void) - -> Status, - segment_number: u32, -} - -/// A field added or dropped above moves `configuration`, and the symptom is -/// firmware calling something else. -const _: () = assert!(core::mem::size_of::() == 18 * 8 + 8); - /// The descriptor list firmware answered with. /// /// Boot services identity-map physical memory, so the pointer is the address @@ -79,9 +46,9 @@ impl Phys for List { /// Every memory window this machine's root bridges decode, written into `out`; /// the count, or zero on a machine that would not say. -pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) -> usize { +pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) -> usize { let bs = system_table.boot_services(); - let handles = match bs.find_handles::() { + let handles = match bs.handles::() { Ok(handles) => handles, Err(e) => { println!("{HEAD} no handle carries the PCI Root Bridge I/O protocol ({e}), so the kernel is handed no window"); @@ -91,7 +58,7 @@ pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) - let mut found = 0usize; for (index, handle) in handles.iter().enumerate() { - let bridge = match crate::protocol::get::(bs, *handle) { + let bridge = match bs.get::(*handle) { Ok(bridge) => bridge, Err(e) => { println!("{HEAD} handle {index} would not open ({e}), so the kernel is handed no window"); @@ -99,19 +66,15 @@ pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) - } }; - let mut resources: *const c_void = core::ptr::null(); - let this: *const PciRootBridgeIo = &*bridge; - // SAFETY: `this` is the protocol firmware installed on this handle and - // the call is the spec's — one out parameter, which firmware fills with - // a pointer it owns and this loader only reads. - let status = unsafe { (bridge.configuration)(this, &mut resources) }; - if !status.is_success() || resources.is_null() { + let answer = bridge.configuration(); + let Some(&resources) = answer.as_ref().ok().filter(|resources| !resources.is_null()) else { println!( - "{HEAD} {index} (segment {}) answered {status:?} to Configuration(), so the kernel is handed no window", - bridge.segment_number + "{HEAD} {index} (segment {}) answered {:?} to Configuration(), so the kernel is handed no window", + bridge.segment_number, + answer.err().unwrap_or(Status::SUCCESS), ); return 0; - } + }; let list = List { at: resources as u64 }; match memory_windows(list, list.at, &mut out[found..]) { diff --git a/bootloader/src/rootimage.rs b/bootloader/src/rootimage.rs index c0f93626e12..dac9adddfbc 100644 --- a/bootloader/src/rootimage.rs +++ b/bootloader/src/rootimage.rs @@ -24,11 +24,7 @@ use core::num::NonZeroU64; use toyos_gpt::{Guid, Located, Sectors}; use toyos_rootimage::chunk; use toyos_update::slots::{self, Table}; -use uefi::prelude::*; -use uefi::proto::device_path::{DevicePath, DevicePathNode, DeviceSubType, DeviceType}; -use uefi::proto::loaded_image::LoadedImage; -use uefi::proto::media::block::{BlockIO, BlockIoProtocol}; -use uefi::table::boot::{AllocateType, MemoryType, ScopedProtocol}; +use crate::efi::{AllocateType, BlockIo, BootServices, DevicePath, Handle, HardDrive, LoadedImage, Scoped, Status}; /// The unit ROOT's filesystem is written in, and the alignment every buffer /// here is allocated at. @@ -44,10 +40,6 @@ const BLOCK: usize = 4096; /// a read that fails is named to within 1 MiB. const CHUNK_BOUND: usize = 1 << 20; -/// UEFI 2.11 §13.9's `EFI_BLOCK_IO_PROTOCOL_REVISION3`, the first whose media -/// carries `OptimalTransferLengthGranularity`. -const BLOCK_IO_REVISION3: u64 = 0x0002_001F; - /// The line a read ROOT is reported on, with where it went and what it cost. pub const READ_AT: &str = "ROOT: read into memory at"; @@ -92,31 +84,30 @@ impl RootImage { /// image from: the one handle whose device path is the partition's without /// its last node, the HARDDRIVE one. pub fn boot_disk(handle: Handle, bs: &BootServices) -> Result { - let image = crate::protocol::exclusive::(bs, handle) + let image = bs + .exclusive::(handle) .map_err(|e| alloc::format!("this image's LoadedImage: {e:?}"))?; let device = image.device().ok_or("firmware names no device this image was loaded from")?; - let path = crate::protocol::get::(bs, device) - .map_err(|e| alloc::format!("the boot device's path: {e:?}"))?; - let nodes: alloc::vec::Vec<&DevicePathNode> = path.node_iter().collect(); + let path = bs.get::(device).map_err(|e| alloc::format!("the boot device's path: {e:?}"))?; + let nodes: alloc::vec::Vec<&[u8]> = path.nodes().collect(); let Some((last, disk_nodes)) = nodes.split_last() else { return Err("the boot device's path is empty".into()); }; - if last.full_type() != (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) { + if (last[0], last[1]) != HardDrive::TYPE { return Err("the boot device is not a partition, so there is no disk to find the slots on".into()); } - let handles = bs - .find_handles::() - .map_err(|e| alloc::format!("firmware lists no block devices: {e:?}"))?; + let handles = bs.handles::().map_err(|e| alloc::format!("firmware lists no block devices: {e:?}"))?; let disks: alloc::vec::Vec = handles - .into_iter() + .iter() + .copied() // Not the partition itself, whose path is held open above: a second // open by this agent is closed along with the first, and the second // close then fails. .filter(|&candidate| candidate != device) .filter(|&candidate| { - let Ok(path) = crate::protocol::get::(bs, candidate) else { return false }; - path.node_iter().eq(disk_nodes.iter().copied()) + let Ok(path) = bs.get::(candidate) else { return false }; + path.nodes().eq(disk_nodes.iter().copied()) }) .collect(); match disks[..] { @@ -127,7 +118,7 @@ pub fn boot_disk(handle: Handle, bs: &BootServices) -> Result { /// The boot disk, read through the firmware's block I/O. pub struct Disk<'a> { - io: ScopedProtocol<'a, BlockIO>, + io: Scoped<'a, BlockIo>, media_id: u32, lba_bytes: u32, lba_count: u64, @@ -138,21 +129,21 @@ pub struct Disk<'a> { impl<'a> Disk<'a> { pub fn open(bs: &'a BootServices, handle: Handle) -> Result { - let io = crate::protocol::get::(bs, handle).map_err(|e| alloc::format!("the boot disk's block I/O: {e:?}"))?; + let io = bs.get::(handle).map_err(|e| alloc::format!("the boot disk's block I/O: {e:?}"))?; let media = io.media(); if !media.is_media_present() { return Err("the boot disk reports no media".into()); } - let lba_bytes = media.block_size(); + let lba_bytes = media.block_size; // UEFI 2.11 §13.9: `IoAlign` is 0 or 1 for none, else a power of two. - let align = media.io_align().max(1) as usize; + let align = media.io_align.max(1) as usize; if !align.is_power_of_two() || align > BLOCK { return Err(alloc::format!("the boot disk wants buffers aligned to {align} bytes")); } if lba_bytes == 0 || !BLOCK.is_multiple_of(lba_bytes as usize) { return Err(alloc::format!("the boot disk's {lba_bytes}-byte block does not divide {BLOCK}")); } - let (media_id, lba_count) = (media.media_id(), media.last_block() + 1); + let (media_id, lba_count) = (media.media_id, media.last_block + 1); Ok(Disk { media_id, lba_bytes, lba_count, io, scratch: aligned(BLOCK) }) } @@ -185,7 +176,7 @@ impl<'a> Disk<'a> { let at = part.first_lba() + i as u64 * lbas; self.io .read_blocks(self.media_id, at, self.scratch) - .map_err(|e| alloc::format!("the slot table's copy {i} would not read: {:?}", e.status()))?; + .map_err(|e| alloc::format!("the slot table's copy {i} would not read: {e:?}"))?; copy.copy_from_slice(self.scratch); } slots::current([&copies[0], &copies[1]]) @@ -212,7 +203,7 @@ impl<'a> Disk<'a> { // `LoaderData`, as the black box's page is, for the reason its module // header gives. let at = bs - .allocate_pages(AllocateType::AnyPages, MemoryType::LOADER_DATA, pages) + .allocate_pages(AllocateType::AnyPages, pages) .map_err(|e| alloc::format!("firmware would not give {len} bytes for ROOT: {e:?}"))?; // SAFETY: the `pages` pages at `at` were just allocated to this loader, // are identity-mapped while boot services live, and nothing else holds them. @@ -231,7 +222,7 @@ impl<'a> Disk<'a> { "the read of {} blocks at LBA {} failed: {:?}, after {} of {len} bytes read", failed.blocks, failed.lba, - failed.error.status(), + failed.error, failed.read ); image.free(bs); @@ -253,27 +244,19 @@ impl<'a> Disk<'a> { /// `OptimalTransferLengthGranularity`, where the media is revision 3 or /// later and so carries the field, and reports a non-zero one. fn granularity_lbas(&self) -> Option { - let io: &BlockIO = &self.io; - // SAFETY: uefi 0.26 declares `BlockIO` `repr(transparent)` over - // `BlockIoProtocol`, so the one is the other's layout; `revision` is - // the field the crate does not expose. - let revision = unsafe { &*core::ptr::from_ref(io).cast::() }.revision; - if revision < BLOCK_IO_REVISION3 { - return None; - } - Some(self.io.media().optimal_transfer_length_granularity()).filter(|&lbas| lbas != 0) + self.io.optimal_transfer_length_granularity().filter(|&lbas| lbas != 0) } } /// The boot disk as [`chunk::read`] asks for it. -struct Firmware<'a> { - io: &'a BlockIO, +struct Firmware<'a, 'b> { + io: &'a Scoped<'b, BlockIo>, media_id: u32, } -impl chunk::Blocks for Firmware<'_> { - type Error = uefi::Error; - fn read(&mut self, lba: u64, into: &mut [u8]) -> uefi::Result { +impl chunk::Blocks for Firmware<'_, '_> { + type Error = Status; + fn read(&mut self, lba: u64, into: &mut [u8]) -> Result<(), Status> { self.io.read_blocks(self.media_id, lba, into) } } diff --git a/bootloader/src/seed.rs b/bootloader/src/seed.rs index 890f8887ae4..c67a58b4cca 100644 --- a/bootloader/src/seed.rs +++ b/bootloader/src/seed.rs @@ -6,30 +6,27 @@ use toyos_abi::boot::SEED_LEN; use toyos_random::{wipe, Seed}; -use uefi::prelude::*; -use uefi::proto::rng::Rng; - -use crate::protocol; +use crate::efi::{Rng, SystemTable}; /// Fill `into` with firmware's seed and answer its length: [`SEED_LEN`], or 0 /// with `into` zero. Judged with the kernel's own [`Seed::judge`], so the line /// here says what the kernel will find. -pub fn read(system_table: &SystemTable, into: &mut [u8; SEED_LEN]) -> u64 { +pub fn read(system_table: &SystemTable, into: &mut [u8; SEED_LEN]) -> u64 { let bs = system_table.boot_services(); let none = |why: core::fmt::Arguments| { println!("Seed: {why}, so the kernel's generator is handed none"); 0 }; - let Ok(handle) = bs.get_handle_for_protocol::() else { + let Ok(handle) = bs.handle_for::() else { return none(format_args!("firmware has no EFI_RNG_PROTOCOL")); }; // GET_PROTOCOL: an exclusive open would stop the driver behind it. - let mut rng = match protocol::get::(bs, handle) { + let rng = match bs.get::(handle) { Ok(rng) => rng, Err(e) => return none(format_args!("EFI_RNG_PROTOCOL would not open ({e})")), }; // No algorithm named: firmware's default (§37.5.2). - if let Err(e) = rng.get_rng(None, into) { + if let Err(e) = rng.get_rng(into) { wipe(into); return none(format_args!("EFI_RNG_PROTOCOL's GetRNG failed ({e})")); } diff --git a/bootloader/src/slot.rs b/bootloader/src/slot.rs index e5130ef5f07..aa1da111752 100644 --- a/bootloader/src/slot.rs +++ b/bootloader/src/slot.rs @@ -27,10 +27,7 @@ use toyos_update::policy::{self, Refusal}; use toyos_update::record::{self, Booted, Record}; use toyos_update::slots::{self, Slot, Which}; use toyos_update::{sig, Digest}; -use uefi::prelude::*; -use uefi::proto::media::file::{File, FileAttribute, FileInfo, FileMode}; -use uefi::proto::media::fs::SimpleFileSystem; -use uefi::CString16; +use crate::efi::{BootServices, CString16, Handle, Mode, SimpleFileSystem, Status, SystemTable}; use crate::rootimage::{Disk, RootImage}; @@ -57,7 +54,7 @@ pub struct Chosen { /// The slot to boot, or every refusal and why there is nothing to boot. pub fn choose( handle: Handle, - system_table: &SystemTable, + system_table: &SystemTable, floor: u64, record: &Record, ) -> Result { @@ -192,7 +189,7 @@ fn signed_header(bs: &BootServices, which: Which, slot: &Slot) -> Result<(Header /// The version the image the record says the last boot proved carries, read /// out of its slot's signed header, verified in this pass; or why no version /// is. -pub fn proven(handle: Handle, system_table: &SystemTable, booted: &Booted) -> Result { +pub fn proven(handle: Handle, system_table: &SystemTable, booted: &Booted) -> Result { let bs = system_table.boot_services(); let letter = booted.slot.letter(); let mut disk = Disk::open(bs, crate::rootimage::boot_disk(handle, bs)?)?; @@ -246,21 +243,22 @@ enum FileRefused { /// `max` bytes. fn read_file(bs: &BootServices, guid: &[u8; 16], path: &str, max: u64) -> Result, FileRefused> { let handle = crate::loaderlog::volume_handle(bs, guid).map_err(FileRefused::Other)?; - let mut fs = crate::protocol::exclusive::(bs, handle) + let mut fs = bs + .exclusive::(handle) .map_err(|e| FileRefused::Other(alloc::format!("would not open its volume ({e})")))?; let mut root = fs.open_volume().map_err(|e| FileRefused::Other(alloc::format!("has no volume ({e})")))?; - let name = CString16::try_from(path.replace('/', "\\").as_str()) - .map_err(|_| FileRefused::Other(String::from("is no UCS-2 path")))?; - let file = match root.open(&name, FileMode::Read, FileAttribute::empty()) { + let name = CString16::new(&path.replace('/', "\\")) + .ok_or_else(|| FileRefused::Other(String::from("is no UCS-2 path")))?; + let file = match root.open(&name, Mode::Read) { Ok(file) => file, - Err(e) if e.status() == Status::NOT_FOUND => return Err(FileRefused::Missing), + Err(Status::NOT_FOUND) => return Err(FileRefused::Missing), Err(e) => return Err(FileRefused::Other(alloc::format!("would not open ({e})"))), }; let mut file = file.into_regular_file().ok_or(FileRefused::Other(String::from("is a directory")))?; - let info = file - .get_boxed_info::() - .map_err(|e| FileRefused::Other(alloc::format!("would not say its size ({e})")))?; - let size = info.file_size(); + let size = file + .info() + .map_err(|e| FileRefused::Other(alloc::format!("would not say its size ({e})")))? + .file_size; if size > max { return Err(FileRefused::Other(alloc::format!("is {size} bytes, past the {max} expected"))); } diff --git a/bootloader/src/stamp.rs b/bootloader/src/stamp.rs index 803d53b291d..e146ab8ac75 100644 --- a/bootloader/src/stamp.rs +++ b/bootloader/src/stamp.rs @@ -35,6 +35,6 @@ pub fn now() -> Head<'static> { /// One line, stamped, to the console and then to `loader.log`. pub fn say(args: fmt::Arguments) { let head = now(); - uefi_services::println!("{head} {args}"); + crate::efi::print(format_args!("{head} {args}\n")); crate::loaderlog::line(format_args!("{head} {args}")); } diff --git a/bootloader/src/wallclock.rs b/bootloader/src/wallclock.rs index 034dbfe886e..12e551feacf 100644 --- a/bootloader/src/wallclock.rs +++ b/bootloader/src/wallclock.rs @@ -7,11 +7,11 @@ use core::fmt; use toyos_wallclock::Civil; -use uefi::prelude::*; +use crate::efi::{Status, SystemTable}; /// Why firmware gave no time. pub enum Refusal { - Failed(uefi::Status), + Failed(Status), NotADate(Civil), } @@ -25,16 +25,16 @@ impl fmt::Display for Refusal { } /// Firmware's time, and [`crate::arch::counter`] as it answered. -pub fn now(system_table: &SystemTable) -> Result<(Civil, u64), Refusal> { - let time = system_table.runtime_services().get_time().map_err(|e| Refusal::Failed(e.status()))?; +pub fn now(system_table: &SystemTable) -> Result<(Civil, u64), Refusal> { + let time = system_table.runtime_services().get_time().map_err(Refusal::Failed)?; let counter = crate::arch::counter(); let civil = Civil { - year: u64::from(time.year()), - month: u64::from(time.month()), - day: u64::from(time.day()), - hour: u64::from(time.hour()), - min: u64::from(time.minute()), - sec: u64::from(time.second()), + year: u64::from(time.year), + month: u64::from(time.month), + day: u64::from(time.day), + hour: u64::from(time.hour), + min: u64::from(time.minute), + sec: u64::from(time.second), }; if !civil.is_valid() { return Err(Refusal::NotADate(civil)); diff --git a/bootloader/src/watchdog.rs b/bootloader/src/watchdog.rs index 2a4d9dbc6a0..2214bd6ed31 100644 --- a/bootloader/src/watchdog.rs +++ b/bootloader/src/watchdog.rs @@ -12,7 +12,6 @@ //! Every machine this cannot arm is refused by name on the console and boots //! anyway. -use core::mem::{align_of, size_of}; use core::ptr::read_volatile; use toyos_acpi::Phys; @@ -20,8 +19,7 @@ use toyos_tco::{ Chipset, TCO1_CNT, TCO1_CNT_LOCK, TCO1_CNT_NO_REBOOT, TCO1_CNT_RUN, TCO1_STS, TCO2_STS, TCO1_STS_TIMEOUT, TCO_RLD, TCO_TMR, }; -use uefi::prelude::*; -use uefi::table::boot::{MemoryDescriptor, PAGE_SIZE}; +use crate::efi::{SystemTable, PAGE_SIZE}; /// x86-64's 52-bit physical-address ceiling. const MAX_PHYS: u64 = 1 << 52; @@ -49,7 +47,7 @@ impl Phys for Identity { /// Arm the watchdog when `cmdline` names it, and say on the console what was /// armed or why nothing was. -pub fn arm(system_table: &SystemTable, rsdp_addr: u64, cmdline: &str) { +pub fn arm(system_table: &SystemTable, rsdp_addr: u64, cmdline: &str) { if !toyos_abi::boot::actuators(cmdline).any(|token| token == toyos_tco::PARAM) { return; } @@ -156,21 +154,14 @@ fn chipset(ecam: u64) -> Option<(&'static Chipset, u32, u32)> { } /// Whether firmware's own map describes `[at, at + len)` inside one region. -fn described(system_table: &SystemTable, at: u64, len: u64) -> bool { +fn described(system_table: &SystemTable, at: u64, len: u64) -> bool { let bs = system_table.boot_services(); - let size = bs.memory_map_size(); + let (map_size, entry_size) = bs.memory_map_size(); // Eight descriptors of slack: the map can grow between the two calls, and // this one allocates in between. - let bytes = size.map_size + 8 * size.entry_size; + let bytes = map_size + 8 * entry_size; let mut words: alloc::vec::Vec = alloc::vec![0; bytes.div_ceil(size_of::())]; - const _: () = assert!(align_of::() <= align_of::()); - // SAFETY: `words` is a live allocation of exactly this many bytes, aligned - // for `u64` and so for `MemoryDescriptor`, and nothing else names it while - // the slice is alive. - let buffer = unsafe { - core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), words.len() * 8) - }; - let Ok(map) = bs.memory_map(buffer) else { return false }; + let Ok(map) = bs.memory_map(&mut words) else { return false }; let Some(end) = at.checked_add(len) else { return false }; // Every field here is firmware's, so a region whose own end overflows // describes nothing rather than wrapping into one that covers `at`. diff --git a/clippy.toml b/clippy.toml index 6f96d7356d0..c83c33fcdf9 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,6 +4,4 @@ disallowed-methods = [ { path = "alloc::sync::Arc::increment_strong_count", reason = "hand-rolled refcounting is the bug class the object layer deletes" }, { path = "alloc::sync::Arc::decrement_strong_count", reason = "hand-rolled refcounting, and the half that frees" }, { path = "core::mem::forget", reason = "a resource nobody gives back is a leak unless its site says why" }, - { path = "uefi::table::boot::BootServices::open_protocol_exclusive", reason = "EXCLUSIVE stops every driver holding the protocol, the firmware's graphics console among them: open through `protocol::exclusive`" }, - { path = "uefi::table::boot::BootServices::open_protocol", reason = "its caller picks the attribute, and EXCLUSIVE or BY_DRIVER stops the driver holding the protocol: open through `protocol::get`" }, ] diff --git a/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md b/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md index b86c3e49ab2..5ebc580d9a1 100644 --- a/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md +++ b/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md @@ -6,8 +6,8 @@ opened: 2026-09-29 # A child's end is an event, and a parent takes its children down -libc cannot start a child: `fork` and `execvp` answer `ENOSYS`, `waitpid` -`ECHILD` and `system` `-1` (`userland/libc/src/misc.rs`, +libc cannot start a child: `fork` and `execvp` answer `ENOSYS`, `waitpid`, +`wait` and `wait4` `ECHILD`, and `system` `-1` (`userland/libc/src/misc.rs`, `userland/libc/src/stdio.rs`), and there is no `posix_spawn`. M2 and M4 of `issues/toyos-builds-itself.md` and the exit of `issues/toyos-runs-on-arm64.md` need it. diff --git a/issues/a-shared-object-does-not-link-against-the-c-sysroot-with-z-defs.md b/issues/a-shared-object-does-not-link-against-the-c-sysroot-with-z-defs.md index 91fb411845f..70dfafc68c8 100644 --- a/issues/a-shared-object-does-not-link-against-the-c-sysroot-with-z-defs.md +++ b/issues/a-shared-object-does-not-link-against-the-c-sysroot-with-z-defs.md @@ -9,12 +9,12 @@ opened: 2026-10-01 ToyOS loads shared objects, a program's `DT_NEEDED` and `dlopen`'s, and its clang links one with `-shared`. None links against the C sysroot with `-z defs`. A C one linked without it leaves `__tls_get_addr` undefined, and -lld refuses the executable that links it for that name. LLVM built for -`x86_64-unknown-toyos` -(`issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`) -builds `libLTO`, `libRemarks`, `libclang` and `libclang-cpp`, linked -`-shared -z defs`, and each stops on what the sysroot's archives were built -for, an executable: +lld refuses the executable that links it for that name. LLVM built whole for +`x86_64-unknown-toyos`, as M3's rustc needs it +(`issues/rustc-llvm-cannot-build-for-a-toyos-host.md`), builds `libLTO`, +`libRemarks`, `libclang` and `libclang-cpp`, linked `-shared -z defs`, none +of which the ToyOS-hosted clang and LLD (`src/hostedclang.rs`) need, and +each stops on what the sysroot's archives were built for, an executable: - `main`, undefined: `libtoyos_c.a`'s entry point, `start_c`, sits in an object the link takes for other names. diff --git a/issues/a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine.md b/issues/a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine.md index 283db974f3d..73a0e53f190 100644 --- a/issues/a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine.md +++ b/issues/a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine.md @@ -8,14 +8,16 @@ opened: 2026-10-03 LLVM takes `LLVM_HOST_TRIPLE` from `config.guess`, run by the CMake that configures it (`llvm/cmake/modules/GetHostTriple.cmake`, -`llvm/cmake/config-ix.cmake`), unless the configuration names one, and -bootstrap names none: the `rust/` fork's -`src/bootstrap/src/core/build_steps/llvm.rs` defines -`LLVM_DEFAULT_TARGET_TRIPLE` and no `LLVM_HOST_TRIPLE`. So the LLVM that M2 -cross-builds to run on ToyOS (`issues/toyos-builds-itself.md`) records -the machine that built it as its host. The configure of that build on the -development Mac, the fork at the commit `main` pins (`95960d6c214`), logged -(#682, comment 5968053849) +`llvm/cmake/config-ix.cmake`), unless the configuration names one. The +ToyOS-hosted clang and LLD name it (`src/hostedclang.rs`), and their +configure logs `LLVM host triple: x86_64-unknown-toyos`. Bootstrap names none: +the `rust/` fork's `src/bootstrap/src/core/build_steps/llvm.rs` defines +`LLVM_DEFAULT_TARGET_TRIPLE` and no `LLVM_HOST_TRIPLE`. So the LLVM that M3's +rustc carries, built by bootstrap for a ToyOS host +(`issues/rustc-llvm-cannot-build-for-a-toyos-host.md`), records the machine +that built it as its host. The configure of that build on the development +Mac, the fork at the commit `main` pinned then (`95960d6c214`), logged (#682, +comment 5968053849) ``` -- LLVM host triple: arm64-apple-darwin27.0.0 @@ -26,8 +28,8 @@ development Mac, the fork at the commit `main` pins (`95960d6c214`), logged it exits non-zero, so a configure run inside ToyOS stops there. That half is read from `GetHostTriple.cmake`, not run. -Owner: `issues/toyos-builds-itself.md`: M2 for the triple the host -build records, M5 for the configure inside ToyOS. +Owner: `issues/toyos-builds-itself.md`: M3 for the triple bootstrap's build +records, M5 for the configure inside ToyOS. -Exit condition: the configure of a ToyOS-hosted LLVM logs `LLVM host triple: -x86_64-unknown-toyos`, and a configure inside ToyOS reaches its end. +Exit condition: bootstrap's configure of a ToyOS-hosted LLVM logs `LLVM host +triple: x86_64-unknown-toyos`, and a configure inside ToyOS reaches its end. diff --git a/issues/an-alarm-reaches-no-sigalrm-handler.md b/issues/an-alarm-reaches-no-sigalrm-handler.md new file mode 100644 index 00000000000..015deb5b6d7 --- /dev/null +++ b/issues/an-alarm-reaches-no-sigalrm-handler.md @@ -0,0 +1,40 @@ +--- +status: open +kind: defect +opened: 2026-09-30 +--- + +# An alarm reaches no SIGALRM handler + +libc's `alarm` (`userland/libc/src/alarm.rs`) arms one alarm per process and +answers the seconds the one it replaces had left, and when it is due a thread +of libc's drops it if `SIGALRM` is ignored and otherwise ends the process with +exit code 142, `SIGALRM`'s default action. POSIX keeps the process in two of +those cases: + +- **A handler.** `signal` and `sigaction` keep `SIGALRM`'s disposition, and a + handler never runs. LLVM bounds its wait on a child with one: a `SIGALRM` + handler makes `wait4` answer `EINTR` (`llvm/lib/Support/Unix/Program.inc`, + `Wait`). +- **A mask.** `SIGALRM` blocked in every thread stays pending until a thread + unblocks it, and libc ends the process at once. libc keeps each thread's + mask in that thread alone (`userland/libc/src/pthread.rs`, `MASK`), and a + thread Rust's std starts is none of libc's, so nothing can read whether + every thread blocks it. + +POSIX gives `alarm` no refusal, and its `SIGALRM` reaches a handler, or waits +pending on a mask, only through the signals libc imitates from stage 3 of +`issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md` on; +both wait on that stage, its owner. + +What a due alarm does under each disposition is held on the host +(`tests/libc-arch/src/alarm_requests.rs`); `207_libc_names.c` reads the +action back whole and arms and disarms an alarm. No guest case lets one come +due. + +**Exit**: a guest C case shows `SIGALRM` delivered as POSIX says: a handler +installed without `SA_RESTART` runs, and a `wait4` on a child that has not +ended answers `EINTR`; with `SIGALRM` blocked in every thread, a due alarm +leaves the process running and `sigpending` names it, until an unblock ends +the process with 142; and with it ignored, a due alarm leaves the process +running. diff --git a/issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md b/issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md index ec3e0ee320c..19e5ad5ea68 100644 --- a/issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md +++ b/issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md @@ -6,32 +6,37 @@ opened: 2026-09-30 # Bootstrap cannot build LLVM, clang and lld for a ToyOS host -M2's clang and lld (`issues/toyos-builds-itself.md`) are bootstrap's -`Llvm` step, with `clang = true`, and its `Lld` step for -`x86_64-unknown-toyos`, in a bootstrap build that names no `llvm-config` for -the build triple and so builds that triple's LLVM, and its `clang-tblgen`, -itself. CMake takes its toolchain file from -`CMAKE_TOOLCHAIN_FILE_x86_64_unknown_toyos`: one that includes the C sysroot's -`toolchain.cmake` and adds the ToyOS LLVM's install to `CMAKE_FIND_ROOT_PATH`, -because the `Lld` step names that LLVM to `find_package` by a hint, and the -sysroot's file has CMake find a package under a root alone. What stops the -build, in the order it stops it: +The track holds one build of one fork (`issues/toyos-builds-itself.md`), and +the tree builds LLVM for a ToyOS host by two paths. M2's clang and `ld.lld` +come from a CMake configure and n2 build of their own (`src/hostedclang.rs`). +M3's rustc carries an LLVM that bootstrap's `Llvm` step builds for +`x86_64-unknown-toyos` (`issues/rustc-llvm-cannot-build-for-a-toyos-host.md`), +and bootstrap's `Llvm` step, with `clang = true`, and its `Lld` step make that +host's clang and lld too. One LLVM, one host, two code paths. -- **Compile.** The first errors are `Support`'s: `Unix/Watchdog.inc` and - `Unix/Program.inc` call `alarm` (`issues/libc-has-no-alarm.md`), and - `Program.inc` stage 3's `wait` and `wait4` - (`issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`). - Then ORC's `shm_open` and `shm_unlink`, the interpreter's `scanf` and +Bootstrap's build names no `llvm-config` for the build triple, and so builds +that triple's LLVM, and its `clang-tblgen`, itself. CMake takes its toolchain +file from `CMAKE_TOOLCHAIN_FILE_x86_64_unknown_toyos`: one that includes the C +sysroot's `toolchain.cmake` and adds the ToyOS LLVM's install to +`CMAKE_FIND_ROOT_PATH`, because the `Lld` step names that LLVM to +`find_package` by a hint, and the sysroot's file has CMake find a package +under a root alone. libc and libc++ now give what stopped a CMake build of +clang and lld from the same commit, measured in #809: `alarm`, `wait`, `wait4`, +`lround` and `std::filesystem`. Bootstrap's build, which builds all of LLVM, +has not run since; what is known to stop it besides: + +- ORC's `shm_open` and `shm_unlink`, the interpreter's `scanf` and `llvm-objdump`'s `ctime` - (`issues/libc-lacks-names-llvm-for-a-toyos-host-calls.md`), and - clang's `std::ifstream`, which libc++ has only with `std::filesystem` - (`issues/libcxx-is-built-without-std-filesystem.md`). -- **Link.** clang needs `lround`, which libc does not define - (`issues/libc-lacks-names-llvm-for-a-toyos-host-calls.md`), beside - those above. LLVM's shared libraries, `libLTO`, `libRemarks`, `libclang` and + (`issues/libc-lacks-names-llvm-for-a-toyos-host-calls.md`). +- LLVM's shared libraries, `libLTO`, `libRemarks`, `libclang` and `libclang-cpp`, do not link against the C sysroot (`issues/a-shared-object-does-not-link-against-the-c-sysroot-with-z-defs.md`). - clang and lld use none of them, and ToyOS's build turns them off. +- Bootstrap configures the build machine as LLVM's host + (`issues/a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine.md`). + +Owner: `issues/toyos-builds-itself.md`, M3. **Exit**: ToyOS's build, with nothing supplied by hand, has bootstrap install -a clang and an `ld.lld` for `x86_64-unknown-toyos`. +a clang and an `ld.lld` for `x86_64-unknown-toyos`, `cargo run -- +--hosted-clang` places those, and `src/hostedclang.rs`'s CMake configure and +build are gone. diff --git a/issues/five-apps-read-the-system-font-by-a-path-only-this-tree-has.md b/issues/five-apps-read-the-system-font-by-a-path-only-this-tree-has.md index 3d790b4ead6..e9d3418eb83 100644 --- a/issues/five-apps-read-the-system-font-by-a-path-only-this-tree-has.md +++ b/issues/five-apps-read-the-system-font-by-a-path-only-this-tree-has.md @@ -12,10 +12,11 @@ ToyOS (root `CLAUDE.md`), and stage 7 of builds each one outside this repository against published SDK crates alone. Five of the six apps cannot, because of how they find JetBrains Mono: -- `editor` (`src/main.rs:1444`), `files` (`:60`) and `paint` (`:173`) read +- `editor` (`src/main.rs:1444`), `files` (`:59`) and `paint` (`:173`) read `/system/share/fonts/JetBrainsMono-Regular-8x16.font` and `expect` it, so on - a host they build and then panic at start; `files` reads - `/system/share/icons/*.svg` the same way (`:63-64`). + a host they build and then panic at start; `files` reads the icons the build + draws, `/system/share/icons/*.alpha`, the same way (`:62-63`, through + `userland/sprite`). - `calc` (`build.rs:32`) and `snake` (`build.rs:7`) read `../../assets/JetBrainsMono-Regular.ttf` in their build scripts, a path into this tree. diff --git a/issues/libc-has-no-alarm.md b/issues/libc-has-no-alarm.md deleted file mode 100644 index 4b2966567e8..00000000000 --- a/issues/libc-has-no-alarm.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# libc has no alarm - -libc neither declares nor defines `alarm`, and LLVM bounds its wait on a child -with one: a `SIGALRM` handler makes `wait4` answer `EINTR` -(`llvm/lib/Support/Unix/Program.inc`, `Wait`), so an LLVM built for ToyOS does -not link without it: rustc's LLVM wrapper linked `-shared -z defs` with the -libraries it needs leaves it undefined, beside stage 3's `wait` and `wait4`. -POSIX gives `alarm` no refusal, and its `SIGALRM` reaches -a handler only through the signals libc imitates from stage 3 of -`issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md` -on; this waits on that stage. - -LLVM's `Wait` disarms it with `alarm(0)` and then restores the old `SIGALRM` -action (`llvm/lib/Support/Unix/Program.inc`), so an `alarm(0)` that disarms -nothing ends the process when the alarm fires under the default action. - -**Exit**: `alarm` arms and disarms `SIGALRM` as POSIX says, which a guest C -case shows: a handler installed without `SA_RESTART` runs, and a `wait4` on a -child that has not ended answers `EINTR`; and after `alarm(5)`, `alarm(0)` -answers from 1 to 5 and a second `alarm(0)` answers 0. diff --git a/issues/libc-lacks-names-llvm-for-a-toyos-host-calls.md b/issues/libc-lacks-names-llvm-for-a-toyos-host-calls.md index eab41ae32f8..a628c47fa84 100644 --- a/issues/libc-lacks-names-llvm-for-a-toyos-host-calls.md +++ b/issues/libc-lacks-names-llvm-for-a-toyos-host-calls.md @@ -6,9 +6,11 @@ opened: 2026-10-01 # libc lacks names LLVM for a ToyOS host calls -LLVM, clang and lld built for `x86_64-unknown-toyos` -(`issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`) -call five names libc neither declares nor defines: +LLVM's tools built for `x86_64-unknown-toyos` call names libc neither +declares nor defines. None is in the clang and `ld.lld` the build makes for +ToyOS (`src/hostedclang.rs`), which compile and link without them; each +stops a build of LLVM's other tools, bootstrap's among them +(`issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`): - `shm_open` and `shm_unlink`, with which ORC maps memory on every Unix but Android (`llvm/lib/ExecutionEngine/Orc/MemoryMapper.cpp`, @@ -16,8 +18,6 @@ call five names libc neither declares nor defines: - `scanf`, which LLVM's interpreter hands an interpreted program (`llvm/lib/ExecutionEngine/Interpreter/ExternalFunctions.cpp`). - `ctime`, with which `llvm-objdump` prints a file's time stamp. -- `lround`, which libc++'s `std::lround` calls, from clang's `Basic` and its - static analyzer: clang does not link without it. **Exit**: libc declares and defines each, doing what POSIX says or refusing in POSIX's form, asserted by a guest C case that reads its effect back. diff --git a/issues/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/libc-refuses-what-toyos-cannot-yet-answer.md index ac3977bd623..4a02e7fd988 100644 --- a/issues/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/libc-refuses-what-toyos-cannot-yet-answer.md @@ -14,6 +14,7 @@ These answer failure in their POSIX form and do nothing - `symlink`, `ENOSYS`: `SYS_SYMLINK` displaces what its name held (`issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md`). - `chmod` and `fchmod`, `ENOSYS`: a file has no mode bits to set. +- `utimes`, `ENOSYS`: no call sets a file's times. - `statvfs` and `fstatvfs`, `ENOSYS`: no call answers a filesystem's size or free space. - `getrlimit` and `setrlimit`, `ENOSYS`: no call answers a process's limits. diff --git a/issues/libcxx-is-built-without-std-filesystem.md b/issues/libcxx-is-built-without-std-filesystem.md deleted file mode 100644 index 7a98601903e..00000000000 --- a/issues/libcxx-is-built-without-std-filesystem.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# libc++ for ToyOS is built without `std::filesystem` - -`src/libcxx.rs` configures the C++ runtime with `LIBCXX_ENABLE_FILESYSTEM=OFF`, -so a ToyOS C++ program that includes `` does not compile, and -`` goes with the option: `std::ifstream` and `std::ofstream` are -undefined templates. Built with it, libc++ asks libc for `setbuf`, `fseeko`, -`ftello`, `utimes`, `truncate`, `pathconf`, `openat`, `unlinkat`, `fdopendir`, -`_PC_PATH_MAX`, `O_DIRECTORY`, `O_NOFOLLOW`, `AT_FDCWD` and `AT_REMOVEDIR`. - -**Exit**: libc carries what libc++'s `src/filesystem` calls, the option goes, -and a guest test lists a directory through `std::filesystem`. diff --git a/issues/no-guest-case-reads-std-filesystem.md b/issues/no-guest-case-reads-std-filesystem.md new file mode 100644 index 00000000000..ef1f371e49c --- /dev/null +++ b/issues/no-guest-case-reads-std-filesystem.md @@ -0,0 +1,20 @@ +--- +status: open +kind: defect +opened: 2026-09-30 +--- + +# No guest case reads `std::filesystem` + +libc++ for ToyOS is built with `std::filesystem`, and `` with it, +over what libc gives `src/filesystem`: `setbuf`, `fseeko`, `ftello`, +`truncate`, `pathconf`'s `_PC_PATH_MAX`, and `utimes`, which refuses +`ENOSYS`, so `last_write_time` sets no time, and `remove_all` walks a +directory by its path (`issues/remove-all-follows-a-link-swapped-in-mid-walk.md`). +Each of libc's is read back by `tests/testcases/tinycc/207_libc_names.c` +and `206_libc_refusals.c`; no C++ program in a guest uses any of it. + +Owner: `issues/toyos-builds-itself.md`, M2. + +**Exit**: a guest test lists a directory through `std::filesystem`, and reads +a file back through `std::ifstream`. diff --git a/issues/remove-all-follows-a-link-swapped-in-mid-walk.md b/issues/remove-all-follows-a-link-swapped-in-mid-walk.md new file mode 100644 index 00000000000..11bcbb68338 --- /dev/null +++ b/issues/remove-all-follows-a-link-swapped-in-mid-walk.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-10-09 +--- + +# `remove_all` follows a link swapped in mid-walk + +Every C++ program on ToyOS gets libc++'s `std::filesystem::remove_all` built +with `-DREMOVE_ALL_USE_DIRECTORY_ITERATOR` (`src/libcxx.rs`): it walks a +directory by its path, and a directory swapped for a symbolic link between the +walk's check and its descent sends the deletes wherever the link points, +outside the tree it was asked to remove. libc++ says so of that walk +(`libcxx/src/filesystem/operations.cpp`, "vulnerable to some race conditions", +https://reviews.llvm.org/D118134), the class of CVE-2022-21658. Its other walk +holds a descriptor for each directory and resolves every name against it +(`openat`, `fdopendir`, `unlinkat`, `O_DIRECTORY`, `O_NOFOLLOW`, +`AT_REMOVEDIR`), and ToyOS has no such descriptor: `open` refuses every +directory, and the kernel resolves every path from the root. + +Owner: `issues/toyos-builds-itself.md`, M2, whose clang brought +`std::filesystem` in. + +**Exit**: a directory opens as a handle that the kernel resolves names +against, libc defines `openat`, `fdopendir` and `unlinkat` over it, and +`src/libcxx.rs` passes no `REMOVE_ALL_USE_DIRECTORY_ITERATOR`. diff --git a/issues/the-build-runs-host-tools-outside-rust-and-qemu.md b/issues/the-build-runs-host-tools-outside-rust-and-qemu.md index 0d4eaa0282d..d89f02160a4 100644 --- a/issues/the-build-runs-host-tools-outside-rust-and-qemu.md +++ b/issues/the-build-runs-host-tools-outside-rust-and-qemu.md @@ -14,12 +14,13 @@ arrives and is not one. M4 and M5 are stages of `issues/toyos-builds-itself.md`. | tool | runs | verdict | exit | |---|---|---|---| -| Python | LLVM's CMake, whenever this host builds an LLVM (`src/llvm.rs`); the C++ runtime's CMake, in every sysroot build (`src/libcxx.rs`), and its build, which runs `libcxx/utils/generate_iwyu_mapping.py` for a header it installs | admitted: no Rust tool does the job, LLVM's CMake requires one (`find_package(Python3 … REQUIRED)` in `rust/src/llvm-project/llvm/CMakeLists.txt`), and so does the runtimes', unconditionally (`runtimes/CMakeLists.txt`): configured as `src/libcxx.rs` does with no Python on `PATH` or in CMake's search, it found none and stopped, exit 1, and with only `python3` added it configured, exit 0 | M5 runs it in the guest | -| CMake | rustc's bootstrap, for LLVM and clang; `src/llvm.rs`, for the LLVM's key; `src/libcxx.rs`, for the C++ runtime in every sysroot build | admitted: no Rust tool does the job, LLVM, clang and LLD are described in CMake, and upstream's only other descriptions are a GN overlay it does not support and a Bazel one | M5 runs it in the guest | -| `sh` running LLVM's `config.guess`, and the POSIX tools and `cc` it runs | LLVM's CMake, whenever this host builds an LLVM, and the C++ runtime's, in every sysroot build, ask it the host's triple, unconditionally (`get_host_triple` in `rust/src/llvm-project/llvm/cmake/modules/GetHostTriple.cmake`, which runs `sh` by name) | refused: a Rust tool does the shell's part, brush 0.4.0: on the development host (macOS, arm64) `config.guess` printed `/bin/sh`'s triple under it, `arm64-apple-darwin27.0.0`, exit 0 each. The script runs `sed`, `uname`, `mktemp`, `grep`, `rm`, `rmdir` and `cc` there under either shell, and that `cc` is the `cc` rows'. Five of the other six are refused, uutils' doing each: under brush with sed 0.2.0, grep 0.2.0 and coreutils 0.12.0's `mktemp`, `rm` and `rmdir`, and nothing else on `PATH` but the host's `uname` and `cc`, it printed that triple, exit 0. `uname` is admitted: coreutils 0.12.0's answers `-p` with `unknown` where macOS's answers `arm`, and `config.guess` reads that as PowerPC, `powerpc-apple-darwin27.0.0`, exit 0 | CMake finds brush as its `sh`, uutils' `sed`, `grep`, `mktemp`, `rm` and `rmdir`, and a Rust `uname` that answers `-p` as the host's does; or M5 runs it in the guest | +| Python | LLVM's CMake, whenever this host builds an LLVM (`src/llvm.rs`) and when it builds the ToyOS-hosted clang and LLD (`src/hostedclang.rs`); the C++ runtime's CMake, in every sysroot build (`src/libcxx.rs`), and its build, which runs `libcxx/utils/generate_iwyu_mapping.py` for a header it installs | admitted: no Rust tool does the job, LLVM's CMake requires one (`find_package(Python3 … REQUIRED)` in `rust/src/llvm-project/llvm/CMakeLists.txt`), and so does the runtimes', unconditionally (`runtimes/CMakeLists.txt`): configured as `src/libcxx.rs` does with no Python on `PATH` or in CMake's search, it found none and stopped, exit 1, and with only `python3` added it configured, exit 0 | M5 runs it in the guest | +| CMake | rustc's bootstrap, for LLVM and clang; `src/llvm.rs`, for the LLVM's key; `src/libcxx.rs`, for the C++ runtime in every sysroot build; `src/hostedclang.rs`, for the ToyOS-hosted clang and LLD and the tablegens of its nested `NATIVE` build | admitted: no Rust tool does the job, LLVM, clang and LLD are described in CMake, and upstream's only other descriptions are a GN overlay it does not support and a Bazel one | M5 runs it in the guest | +| `sh` running LLVM's `config.guess`, and the POSIX tools and `cc` it runs | LLVM's CMake, whenever this host builds an LLVM, the nested `NATIVE` configure of the ToyOS-hosted clang's build (`src/hostedclang.rs`), which is named no `LLVM_HOST_TRIPLE`, and the C++ runtime's, in every sysroot build, ask it the host's triple, unconditionally (`get_host_triple` in `rust/src/llvm-project/llvm/cmake/modules/GetHostTriple.cmake`, which runs `sh` by name) | refused: a Rust tool does the shell's part, brush 0.4.0: on the development host (macOS, arm64) `config.guess` printed `/bin/sh`'s triple under it, `arm64-apple-darwin27.0.0`, exit 0 each. The script runs `sed`, `uname`, `mktemp`, `grep`, `rm`, `rmdir` and `cc` there under either shell, and that `cc` is the `cc` rows'. Five of the other six are refused, uutils' doing each: under brush with sed 0.2.0, grep 0.2.0 and coreutils 0.12.0's `mktemp`, `rm` and `rmdir`, and nothing else on `PATH` but the host's `uname` and `cc`, it printed that triple, exit 0. `uname` is admitted: coreutils 0.12.0's answers `-p` with `unknown` where macOS's answers `arm`, and `config.guess` reads that as PowerPC, `powerpc-apple-darwin27.0.0`, exit 0 | CMake finds brush as its `sh`, uutils' `sed`, `grep`, `mktemp`, `rm` and `rmdir`, and a Rust `uname` that answers `-p` as the host's does; or M5 runs it in the guest | | `git` for worktrees, submodules, checkouts, fixtures and rustc's bootstrap | adds worktrees (`src/sysroot.rs`); updates submodules (`src/lib.rs`, `src/sysroot.rs`); fetches the fork from the primary's and checks it out (`src/sysroot.rs`); makes the tests' fixture repositories; runs inside rustc's bootstrap | admitted: no Rust tool does the job, gitoxide 0.85 adds, removes and prunes no worktree, updates no submodule, stages, resets and pushes nothing, checks out only a fresh clone and fetches a local path by spawning `git`; a fixture must be what `git` makes, and bootstrap runs `git` itself | M4 runs it in the guest | -| `git` for reads, a config write, a commit's paths written out, and clones and fetches over HTTPS | `rev-parse`, `merge-base`, `ls-tree`, `ls-files`, `cat-file`, `config --get-regexp`, `config --blob`, `status`, `diff`, `ls-remote` and `grep`, in the build system and its tests; `config --global --add safe.directory` in CI's containers; `checkout -- ` through an index of its own, which writes the C++ runtime's sources out of the LLVM commit into the stored LLVM (`src/llvm.rs`); every workflow's checkout | refused: a Rust tool does it, gitoxide 0.85, which reads refs, objects, the index, config, worktrees and status, adds a value to a config file and writes it (gix-config 0.58's `File::section_mut_or_create_new`, `SectionMut::push`, `File::write_to`), walks history, diffs, and lists, fetches and clones a remote over HTTPS; `grep` is a search of the files its index names; and gitoxide's CLI 0.59 (gix 0.88) wrote the runtimes' sources of LLVM `849da7d6` into an empty directory, each path's tree through `gix rev parse`, `gix index from-tree` and `gix free index checkout-exclusive`, exit 0 each: the 18759 files `git` writes there, byte for byte and mode for mode | those are gitoxide's | -| `cc`, `c++` and `ar` on a Linux host, `build-essential` in CI's containers | rustc links every host binary through `cc`; `cc` and `c++` compile LLVM, clang, LLD and `rustc_llvm` (`src/llvm.rs` names both to bootstrap); `cc` compiles ring's C and assembly in every build of the build system itself, whose HTTP agent is rustls on ring (`src/release.rs`), and in every build of doom's build script, whose download of doomgeneric is `ureq` on rustls on ring (`userland/doom/build.rs`); `ar` archives what `cc::Build` compiles, ring's included | admitted: no Rust tool compiles C or C++, or takes rustc's host link; ring is the one TLS provider the tree takes (owner, 2026-10-02), over a provider in Rust alone | M5: no host in the loop | +| `git` for reads, a config write, a commit's paths written out, and clones and fetches over HTTPS | `rev-parse`, `merge-base`, `ls-tree`, `ls-files`, `cat-file`, `config --get-regexp`, `config --blob`, `status`, `diff`, `ls-remote` and `grep`, in the build system and its tests; `config --global --add safe.directory` in CI's containers; `checkout -- ` through an index of its own, which writes the C++ runtime's sources out of the LLVM commit into the stored LLVM, and the ToyOS-hosted clang's out of it into its build (`src/llvm.rs`, `src/hostedclang.rs`); every workflow's checkout | refused: a Rust tool does it, gitoxide 0.85, which reads refs, objects, the index, config, worktrees and status, adds a value to a config file and writes it (gix-config 0.58's `File::section_mut_or_create_new`, `SectionMut::push`, `File::write_to`), walks history, diffs, and lists, fetches and clones a remote over HTTPS; `grep` is a search of the files its index names; and gitoxide's CLI 0.59 (gix 0.88) wrote the runtimes' sources of LLVM `849da7d6` into an empty directory, each path's tree through `gix rev parse`, `gix index from-tree` and `gix free index checkout-exclusive`, exit 0 each: the 18759 files `git` writes there, byte for byte and mode for mode | those are gitoxide's | +| `git log` and `git status` for os-release | the commit, its committer time and whether the tree is that commit's, which `src/build.rs`'s `release` writes into `/system/etc/os-release` in every image build | admitted: the owner's ruling on the dependency audit (2026-10-09), "Drop gix, use git", which covers these two reads and nothing else this table names: they were gitoxide's, gitoxide is no tool the build sheds while `git` stays for the worktree row, and for them it held 84 of the root `Cargo.lock`'s packages | M4 runs it in the guest | +| `cc`, `c++` and `ar` on a Linux host, `build-essential` in CI's containers | rustc links every host binary through `cc`; `cc` and `c++` compile LLVM, clang, LLD and `rustc_llvm` (`src/llvm.rs` names both to bootstrap), and the tablegens of the ToyOS-hosted clang's `NATIVE` build (`src/hostedclang.rs`); `cc` compiles ring's C and assembly in every build of the build system itself, whose HTTP agent is rustls on ring (`src/release.rs`), and in every build of doom's build script, whose download of doomgeneric is `ureq` on rustls on ring (`userland/doom/build.rs`); `ar` archives what `cc::Build` compiles, ring's included | admitted: no Rust tool compiles C or C++, or takes rustc's host link; ring is the one TLS provider the tree takes (owner, 2026-10-02), over a provider in Rust alone | M5: no host in the loop | | the toolchain's own `clang`, `llvm-ar`, `rust-lld` and `llvm-config`, built from `ToyOSOrg/llvm-project` | rustc links every guest binary with `rust-lld`; `clang` compiles the C corpus (`tests/common/compile.rs`) and, with `llvm-ar`, through `cc::Build` the C of every guest crate that has any (`src/clang.rs`, `src/build.rs`'s `GuestEnv`): doomgeneric, and ring's C and assembly in a test crate's `https_get` and `ring_kat`; rustc's bootstrap asks `llvm-config` how to link LLVM | admitted: our fork's C++, which ToyOS can one day build and run; no Rust tool compiles C, `cc::Build` archives with an `ar`, bootstrap reads LLVM through `llvm-config`, and `CLAUDE.md` links everything with `rust-lld` | M5: no host in the loop | | `ovmf-generic`, `qemu-efi-aarch64` | the x86-64 and AArch64 UEFI firmware of CI's guest containers (`src/firmware.rs`), packaged by Debian apart from QEMU | admitted: QEMU's own firmware, and no Rust firmware does its job | the instrument's QEMU carries its own firmware | | `ca-certificates` | the trust store `git` and `curl` verify against in CI's containers | admitted: data both of them need | goes when neither runs there | diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index 9c05b2a802f..8ade7486848 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -13,7 +13,9 @@ The owner's bounds: - the kernel calls no UEFI service, and every UEFI call ToyOS makes is the loader's, before `ExitBootServices` (root `CLAUDE.md`); - the anti-rollback floor counts a signed security version, raised only by a - release that fixes a security hole. + release that fixes a security hole; +- the loader calls UEFI through its own bindings, never the `uefi` crate's + (the owner's ruling on the dependency audit). PR #539 does not land. Its pieces: @@ -83,7 +85,10 @@ Each stage lands on its own, in this order. 2. **One boot disk.** - `toyos_update::entry::partition` is the one HARDDRIVE rule, taken by - `boot_partition`, `rootimage::boot_disk` and `bootnext.rs`. + `boot_partition`, `rootimage::boot_disk` and `bootnext.rs`. The loader's + two decoders go into it: `efi::HardDrive::parse` (`efi/proto.rs`), which + reads firmware-built paths, and `bootnext::gpt_signature`, which reads a + load option's. - Every volume the loader opens (the slot's FAT partition, the log partition and the attempts file on it) is found on the boot disk, where exactly one match is taken. `loaderlog::volume_handle`'s machine-wide @@ -94,7 +99,8 @@ Each stage lands on its own, in this order. one `Disk::open` and slot-table read, and one file reader. `load_file_bytes`, `MAX_ESP_FILE` and the unsound `alloc_uninit` go. - **Exit**: `a_path_names_the_partition_of_its_one_hard_drive_node` and + **Exit**: `rg 'fn gpt_signature|fn parse' bootloader/src/{bootnext.rs,efi/proto.rs}` + finds nothing. `a_path_names_the_partition_of_its_one_hard_drive_node` and `a_partitions_disk_is_the_path_before_its_hard_drive_node` pass on the host, and fail under two mutations: cutting the disk before the path's last node, and taking a second HARDDRIVE node. `root_named_twice` plants a twin whose @@ -145,13 +151,13 @@ Each stage lands on its own, in this order. (`fwvars::live` in `tests/common/fwvars.rs`): each floor's name and its UEFI 2.10 §8.2 attributes. -4. **The loader on current `uefi`, sound, with a typed handover.** - - `uefi` and `uefi-raw` move to their current releases, and `uefi-services` - goes. The unsafe `BlockIO` media cast in `rootimage.rs` goes with the old - layout. - - The loader's own `#[panic_handler]` writes `loader: panicked at +4. **The loader sound, with a typed handover.** + - The loader's `#[panic_handler]` writes `loader: panicked at :: ` through `loaderlog`, then powers the machine off. It never resets: a panic with a fixed cause would reset into itself. + From `EXITING` on (the first `ExitBootServices` call) it skips `loaderlog` + and goes straight to `ResetSystem`'s shutdown, since UEFI 2.10 §7.4.6 + forbids the File protocol from that call on as it forbids the console. The refused-floor site stops writing its reason to `loader.log` before its `panic!`: the handler writes it. - `alloc_kernel_memory` stops building a `Vec` over a 2 MiB-aligned @@ -181,11 +187,11 @@ Each stage lands on its own, in this order. change. - `toyos-update`'s slot table takes `toyos-gpt`'s CRC32 and loses its own. - **Exit**: `bootloader/Cargo.toml` names no `uefi-services`. - `loader_panic_powers_off` plants this key's floor in 9 bytes. It finds the + **Exit**: `loader_panic_powers_off` plants this key's floor in 9 bytes. It finds the handler's `loader: panicked at bootloader/src/` line in `loader.log`, which no other code writes, and QEMU reports `guest-shutdown`. It fails under - `uefi::helpers`' handler, which writes no `loader.log`. + the handler `bootloader/src/efi/mod.rs` holds before this stage, which + writes no `loader.log`. `kernel_args_last_layout_refused` boots with `loader-writes-the-last-layout` and finds the kernel's refusal naming both words before any `black box:` record. Moving `layout` after diff --git a/issues/the-tree-says-who-uses-each-thing.md b/issues/the-tree-says-who-uses-each-thing.md index 426f148efed..921ba1fe488 100644 --- a/issues/the-tree-says-who-uses-each-thing.md +++ b/issues/the-tree-says-who-uses-each-thing.md @@ -121,7 +121,7 @@ Cargo.toml Cargo.lock clippy.toml .cargo/config.toml rule step 1: on loader/ the UEFI loader (today bootloader/) kernel/ CLAUDE.md; src/ pure/ loom/ sim/ pci/ gicv3/ ps2/ dma/ cpuvuln/ microcode/ (+ intel-ucode, licence) system/ CLAUDE.md (the server doctrine); one directory per /system/bin name: - supervisor compositor (desktop/ sprite/ wallpaper.jpg) + supervisor compositor (desktop/ sprite/) netstack (mdns/ dns/ and the net family) soundserver (mixer/) console diskserver fileserver logkeeper filepicker pkg update swap sshserver shell terminal toybox inspect host apps/ CLAUDE.md (sdk/ and registries only; Linux under Wayland, macOS, Windows) diff --git a/issues/toyos-builds-itself.md b/issues/toyos-builds-itself.md index 02a51e6940b..c04a8ea8e0a 100644 --- a/issues/toyos-builds-itself.md +++ b/issues/toyos-builds-itself.md @@ -38,6 +38,10 @@ AArch64 one step behind, on `issues/toyos-runs-on-arm64.md`'s track. - Make it work, then optimise, then compare: no performance study now, and no comparison before a compilation inside ToyOS succeeds. +**Decided** (owner, 2026-10-09). M2 installs clang and lld from a local file +first, without waiting for packages over HTTPS, and links them statically: +"Yes, decoupled and static (Recommended)". + **To build** (2026-10-01), in an order that is open. Where a stage names libc's state it is `userland/libc` at `15625e0cb`. - **The `libc` crate gains a ToyOS module**, checked by `ctest` against @@ -99,12 +103,14 @@ libc's state it is `userland/libc` at `15625e0cb`. how it is split into packages, and how a self-host test's guest reaches them. -**Blocked on other tracks.** M2 needs packages over HTTPS +**Blocked on other tracks.** M2 needs room for about a gigabyte of toolchain, +and threads and `mmap` mature enough for LLVM +(`issues/std-and-libc-drop-the-answer-thread-join-gives.md`). Packages over +HTTPS (`issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md`) -and the network stack under it (`issues/the-lan-is-not-yet-production-grade.md`, -`issues/the-internet-clients-work-unchanged.md`), room for about a -gigabyte of toolchain, and threads and `mmap` mature enough for LLVM -(`issues/std-and-libc-drop-the-answer-thread-join-gives.md`). +wait on the network stack under them +(`issues/the-lan-is-not-yet-production-grade.md`, +`issues/the-internet-clients-work-unchanged.md`), and M2 does not. M2 and M4 also need libc to start a child process (`issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`). M4 needs git in the guest, storage durable and fast enough for an LLVM build tree @@ -119,16 +125,18 @@ the ToyOS-hosted rustc names no linker the guest has (`issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md`). *Exit*: the commit that lands M2's compile-and-run test, which links inside the guest. -**What stops M2: LLVM, clang and lld built for a ToyOS host**, in the order -each blocks the next, as -`issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md` -measures it. +**What stops M2: clang and lld running inside ToyOS.** `cargo run -- +--hosted-clang` makes a clang and an `ld.lld` for `x86_64-unknown-toyos`, +static PIEs that name no library (`src/hostedclang.rs`): 147.5 MB and +85.6 MB, 121.9 MB and 70.9 MB stripped. No guest has run either. - Run: what `issues/libc-refuses-what-toyos-cannot-yet-answer.md` lists, `issues/libc-has-no-pread-or-pwrite.md`, and `issues/libc-stat-answers-one-serial-number-for-every-file.md`. **What M3 adds: a rustc that carries that LLVM**, after all of M2's. -- Build: `issues/rustc-llvm-cannot-build-for-a-toyos-host.md`. +- Build: `issues/rustc-llvm-cannot-build-for-a-toyos-host.md`, and + `issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`, + whose build of clang and lld replaces M2's. - Link: `issues/a-rust-std-binary-cannot-link-the-cxx-runtime.md`. - Test: `issues/nothing-builds-the-toyos-hosted-rustc.md`. @@ -136,7 +144,7 @@ M3's exit then waits on a linker in the guest (`issues/the-hosted-rustc-names-a-linker-toyos-does-not-have.md`). **Also owed, by milestone.** -- M2: the host triple a ToyOS-hosted LLVM records +- M3: the host triple the ToyOS-hosted LLVM bootstrap builds records (`issues/a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine.md`, whose other half, the configure inside ToyOS, is M5's). - M3: `issues/the-hosted-rustcs-stage2-carries-seventeen-proc-macro-libraries-no-image-needs.md`. diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 5ccf684ed65..5c923bec491 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -182,8 +182,8 @@ refuses anything but `EM_X86_64` (`toyos-elf/src/header.rs:24,75`). `irq_guard`, `halt`, `need_resched`, `switch`), with `kernel/src/arch/x86_64/hw.rs` as the one x86 implementation and a simulator as the other. PCI is ECAM/MMIO-only (`drivers/pci.rs:134-154`), no `0xCF8`. NVMe, xHCI and virtio -have no ISA dependence beyond TSC-based waits. The bootloader is the `uefi` -crate (0.26, aarch64-capable already); only 12 of its 2,881 lines are x86. +have no ISA dependence beyond TSC-based waits. The bootloader calls UEFI +through its own bindings, which boot both targets. The pure decision crates carry no arch at all: `toyos-acpi`, `-gpt`, `-fat32`, `-dma`, `-userbound`, `-proclife`, `-blackbox`, `-elide` and others. diff --git a/src/assets.rs b/src/assets.rs index 55f3fb77201..33f57a98768 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -242,16 +242,30 @@ fn absentees(dir: &Path, declared: &BTreeSet) -> Vec { declared.iter().map(|name| dir.join(name)).filter(|path| !path.exists()).collect() } -/// An asset on ROOT, and the one program that opens it. +/// A file of ROOT's `share/`, and the one program that opens it. /// /// **`assets = [..]` names a directory and sweeps it whole**, so a config that -/// builds no reader for a file still shipped it: these two are 19.7 MB of the -/// 20.8 MB `assets/` holds. Named here rather than per config, because which program -/// opens a file is a property of the program and not of any one image, and a -/// list repeated in five configs is a list that goes stale in four of them. -/// The names are ROOT's, which [`collect`] lower-cases. -/// `only_doom_opens_doom_s_assets` is what keeps the right-hand column true. -const OPENED_BY: &[(&str, &str)] = &[("doom1.wad", "doom"), ("soundfont.sf2", "doom")]; +/// builds no reader for a file would still ship it. Named here rather than per +/// config, because which program opens a file is a property of the program and +/// not of any one image, and a list repeated in five configs is a list that goes +/// stale in four of them. The names are ROOT's, which [`collect`] lower-cases; +/// `wallpaper.rgb` is the build's own drawing ([`crate::wallpaper`]) rather than +/// a swept file. `only_its_reader_opens_an_owned_file` is what keeps the +/// right-hand column true. +const OPENED_BY: &[(&str, &str)] = + &[("doom1.wad", "doom"), ("soundfont.sf2", "doom"), ("wallpaper.rgb", "compositor")]; + +/// Whether ROOT's `share/` goes into an image building exactly +/// `programs`: unless [`OPENED_BY`] names a reader the image does not build. +pub fn wanted(name: &str, programs: &BTreeSet<&str>) -> bool { + match OPENED_BY.iter().find(|(file, _)| *file == name) { + Some((_, reader)) if !programs.contains(reader) => { + eprintln!("assets: leaving out share/{name} — only /system/bin/{reader} opens it and this image builds no {reader}"); + false + } + _ => true, + } +} /// ROOT's asset files, for an image building exactly `programs`. pub fn collect(dirs: &[String], programs: &BTreeSet<&str>) -> Vec<(String, Vec)> { @@ -274,18 +288,7 @@ pub fn collect(dirs: &[String], programs: &BTreeSet<&str>) -> Vec<(String, Vec) -> Vec<(String, Vec) -> Vec<(String, Vec = collect(&[dir.display().to_string()], &BTreeSet::new()) .into_iter() @@ -416,7 +411,7 @@ mod tests { shipped, BTreeSet::from([ "share/kept.wad".to_string(), - "share/icons/kept.svg".to_string(), + "share/icons/x-bold.alpha".to_string(), "share/music.sf2".to_string(), ]), "ROOT's asset list is not what the repository says it is" @@ -437,7 +432,7 @@ mod tests { without, BTreeSet::from([ "share/kept.wad".to_string(), - "share/icons/kept.svg".to_string(), + "share/icons/x-bold.alpha".to_string(), ]), "a committed asset that is not there took something else with it" ); @@ -453,7 +448,7 @@ mod tests { #[test] fn an_owned_asset_ships_only_where_its_reader_does() { let dir = toyos_tmpdir::TempDir::new("owned"); - for name in ["doom1.wad", "soundfont.sf2", "wallpaper.rgb"] { + for name in ["doom1.wad", "soundfont.sf2", "readme.txt"] { fs::write(dir.join(name), b"tracked").unwrap_or_else(|e| panic!("write {name}: {e}")); } let git = |args: &[&str]| { @@ -465,7 +460,7 @@ mod tests { assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); }; git(&["init", "-q"]); - git(&["add", "doom1.wad", "soundfont.sf2", "wallpaper.rgb"]); + git(&["add", "doom1.wad", "soundfont.sf2", "readme.txt"]); let shipped = |programs: BTreeSet<&str>| -> BTreeSet { collect(&[dir.display().to_string()], &programs) @@ -481,13 +476,13 @@ mod tests { BTreeSet::from([ "share/doom1.wad".to_string(), "share/soundfont.sf2".to_string(), - "share/wallpaper.rgb".to_string(), + "share/readme.txt".to_string(), ]), "an image that builds doom did not get doom's assets" ); assert_eq!( without, - BTreeSet::from(["share/wallpaper.rgb".to_string()]), + BTreeSet::from(["share/readme.txt".to_string()]), "an image with no doom in it still carries what only doom opens" ); } @@ -552,10 +547,9 @@ mod tests { /// /// The claim is that one program opens the file, so a second program naming /// its path is an image losing a file it needs. `userland/` only: the - /// harness names both paths in assertions about doom, and it runs on the - /// host. + /// harness names doom's paths in its assertions, and it runs on the host. #[test] - fn only_doom_opens_doom_s_assets() { + fn only_its_reader_opens_an_owned_file() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); for (asset, reader) in OPENED_BY { let out = Command::new("git") diff --git a/src/build.rs b/src/build.rs index cb37eb243dd..f04e5dc0a80 100644 --- a/src/build.rs +++ b/src/build.rs @@ -675,43 +675,27 @@ fn render_manifest(config: &SystemConfig) -> Vec { /// Which build `root`'s tree makes for `arch` against the sysroot whose key /// for it is `toolchain`, as `/system/etc/os-release` records it, read with -/// gitoxide (`issues/the-build-runs-host-tools-outside-rust-and-qemu.md`, -/// row 21). Untracked files are dirty whatever `status.showUntrackedFiles` +/// `git`. Untracked files are dirty whatever `status.showUntrackedFiles` /// says; submodules are not read, because the fork's state is the toolchain /// key's. fn release(root: &Path, toolchain: &crate::keystore::Key, arch: Arch) -> toyos_osrelease::Release { - let repo = gix::open(root).unwrap_or_else(|e| panic!("{} is no git checkout: {e}", root.display())); - let head = repo.head_commit().unwrap_or_else(|e| panic!("{}'s HEAD names no commit: {e}", root.display())); - let commit = toyos_osrelease::Hex::parse(&head.id.to_string()).expect("a SHA-1 commit is forty hex digits"); - let time = head.time().unwrap_or_else(|e| panic!("commit {} names no committer time: {e}", head.id)); - let committed = u64::try_from(time.seconds) - .unwrap_or_else(|_| panic!("commit {} was committed before 1970: {}", head.id, time.seconds)); - // Set whole: the platform `status` makes has no walk at all for a - // checkout configured to show no untracked files. - let walk = repo - .dirwalk_options() - .unwrap_or_else(|e| panic!("the status of {}: {e}", root.display())) - .emit_untracked(gix::dir::walk::EmissionMode::CollapseDirectory); - let changes = repo - .status(gix::progress::Discard) - .and_then(|status| { - status - .index_worktree_options_mut(|options| options.dirwalk_options = Some(walk)) - .index_worktree_submodules(None) - .into_iter(None) - }) - .unwrap_or_else(|e| panic!("the status of {}: {e}", root.display())); - // An index entry whose stat alone moved, and an ignored file the walk - // passed, summarise to nothing. - let dirty = changes.map(|item| item.unwrap_or_else(|e| panic!("the status of {}: {e}", root.display()))).any( - |item| match item { - gix::status::Item::IndexWorktree(change) => change.summary().is_some(), - gix::status::Item::TreeIndex(_) => true, - }, + let head = crate::sysroot::git_out(root, &["log", "-1", "--no-show-signature", "--format=%H %ct", "HEAD"]); + let (id, time) = head + .trim() + .split_once(' ') + .unwrap_or_else(|| panic!("{}'s HEAD names no commit and time: {head:?}", root.display())); + let commit = toyos_osrelease::Hex::parse(id).expect("a SHA-1 commit is forty hex digits"); + let committed: u64 = + time.parse().unwrap_or_else(|_| panic!("commit {id} was not committed after 1970: {time}")); + // `--no-optional-locks`, because a status otherwise rewrites the index it + // refreshes, under the lock a concurrent `git` in this checkout takes. + let status = crate::sysroot::git_bytes( + root, + &["--no-optional-locks", "status", "--porcelain", "--untracked-files=normal", "--ignore-submodules=all"], ); toyos_osrelease::Release { commit, - tree: if dirty { toyos_osrelease::Tree::Dirty } else { toyos_osrelease::Tree::Clean }, + tree: if status.is_empty() { toyos_osrelease::Tree::Clean } else { toyos_osrelease::Tree::Dirty }, toolchain: toyos_osrelease::Hex::parse(toolchain.as_str()).expect("a key is sixteen hex digits"), arch: match arch { Arch::X86_64 => toyos_osrelease::Arch::X86_64, @@ -760,10 +744,13 @@ fn build_and_assemble( build_programs(root, config, env, quiet, arch, &mut root_files); root_files.push((toyos_manifest::PATH.to_string(), render_manifest(config))); + let programs: BTreeSet<&str> = config.programs.keys().map(String::as_str).collect(); if !config.assets.is_empty() { - let programs: BTreeSet<&str> = config.programs.keys().map(String::as_str).collect(); root_files.extend(assets::collect(&config.assets, &programs)); } + if assets::wanted(crate::wallpaper::NAME, &programs) { + root_files.push((format!("share/{}", crate::wallpaper::NAME), crate::wallpaper::rgb())); + } root_files.push((TRUST_ROOTS.to_string(), trust_roots())); root_files.push((TRUST_ROOTS_LICENCE.0.to_string(), TRUST_ROOTS_LICENCE.1.to_vec())); @@ -2143,7 +2130,7 @@ mod tests { .status() .unwrap(); assert!(committed.success()); - // The ref as `git` wrote it, for an oracle that is not gitoxide. + // The ref as `git` wrote it, read off the file rather than asked of `git`. let head = fs::read_to_string(work.join(".git/refs/heads/wt")).unwrap(); let key = crate::keystore::Key::parse("0123456789abcdef").unwrap(); diff --git a/src/buildlock.rs b/src/buildlock.rs index 95920dbee39..d6c679c9640 100644 --- a/src/buildlock.rs +++ b/src/buildlock.rs @@ -28,11 +28,13 @@ //! removed, and its modification time is when the key was last used or made. //! //! A compiler key's lock → a sysroot key's lock → a freestanding key's lock → the -//! worktree build lock → an LLVM key's lock → artifact. A compiler's, a -//! sysroot's or a freestanding key lock is taken with the worktree lock put -//! down ([`Held::without_shared`]), because the key's builder takes the -//! worktree lock exclusively; an LLVM key's is taken inside the worktree lock -//! covering the fork build directory its builder writes. +//! worktree build lock → an LLVM key's lock → a ToyOS-hosted clang key's lock +//! → artifact. A compiler's, a sysroot's or a freestanding key lock is taken +//! with the worktree lock put down ([`Held::without_shared`]), because the +//! key's builder takes the worktree lock exclusively; an LLVM key's is taken +//! inside the worktree lock covering the fork build directory its builder +//! writes; a ToyOS-hosted clang key's inside the worktree lock and the use of +//! the sysroot it builds against, writing only the store. //! //! Holder death: `flock` is released by the kernel when the open file //! description closes, so a builder that is SIGKILLed mid-phase — routine here @@ -171,13 +173,15 @@ pub fn artifact(root: &Path) -> Guard { /// A content-addressed product of the host, locked per key: a sysroot, the /// freestanding targets' libraries it carries (`src/sysroot.rs`), a compiler -/// (`src/compiler.rs`), or the LLVM a compiler links (`src/llvm.rs`). +/// (`src/compiler.rs`), the LLVM a compiler links (`src/llvm.rs`), or the +/// clang and LLD built to run on ToyOS (`src/hostedclang.rs`). #[derive(Clone, Copy)] pub enum Keyed { Sysroot, Freestanding, Compiler, Llvm, + HostedClang, } impl Keyed { @@ -187,6 +191,7 @@ impl Keyed { Keyed::Freestanding => "freestanding", Keyed::Compiler => "compilers", Keyed::Llvm => "llvm", + Keyed::HostedClang => "hosted-clang", } } @@ -201,6 +206,7 @@ impl Keyed { Keyed::Freestanding => "freestanding libraries", Keyed::Compiler => "compiler", Keyed::Llvm => "LLVM", + Keyed::HostedClang => "ToyOS-hosted clang", } } } @@ -215,7 +221,7 @@ fn keyed_building(store: &Path, kind: Keyed, key: &Key) -> Guard { /// Use what `key` names: shared, so any number of builds use it at once, a /// builder of it is waited for, and a sweep cannot remove it; and dated now, /// which a sweep reads as its last use. -fn keyed_using(store: &Path, kind: Keyed, key: &Key) -> Guard { +pub(crate) fn keyed_using(store: &Path, kind: Keyed, key: &Key) -> Guard { let path = keyed_lock_path(store, kind, key); let file = open_lock_file(&path); if !try_lock(&file, LOCK_SH) { @@ -596,6 +602,16 @@ pub(crate) mod tests { Elsewhere::hold("buildlock::tests::child_role", &env) } + /// `root`'s worktree lock held shared by a process of its own. + pub(crate) fn shared_elsewhere(root: &Path) -> Elsewhere { + held_elsewhere(root, "hold-shared") + } + + /// Whether `root`'s worktree lock would keep a shared acquirer out now. + pub(crate) fn keeps_out_shared(root: &Path) -> bool { + !try_lock(&open_lock_file(&worktree_lock_dir(root).join("state")), LOCK_SH) + } + /// What `role` of [`child_role`] takes in `root`, held by a process of its own. fn held_elsewhere(root: &Path, role: &str) -> Elsewhere { Elsewhere::hold("buildlock::tests::child_role", &[(ROLE, OsStr::new(role)), (ROOT, root.as_os_str())]) @@ -701,6 +717,10 @@ pub(crate) mod tests { let mut held = shared(&root, "child"); held.act_if("queued exclusive phase", || Some(()), |()| note(&root, "ex")); } + "hold-shared" => { + let _held = shared(&root, "child"); + hold_until_released(); + } "want-shared" => { let _held = shared(&root, "child"); note(&root, "sh"); diff --git a/src/fatformat.rs b/src/fatformat.rs new file mode 100644 index 00000000000..b1219263b52 --- /dev/null +++ b/src/fatformat.rs @@ -0,0 +1,194 @@ +//! An empty FAT32 volume, the one the build lays every FAT partition down as +//! before `toyos-fat32` writes its files: a boot sector and its backup, an +//! FSInfo sector, two FATs and a root directory holding the volume label. +//! +//! Here and not in `toyos-fat32`, which is the kernel's driver and has no +//! format path by design. The geometry is fatgen103's with the choices of the +//! `fatfs` crate this replaced, so every volume is laid out as before: +//! 512-byte sectors, eight reserved sectors, the cluster size by volume size, +//! and the FAT sized for the clusters left once the FATs are paid for. +//! `differs_from_the_fatfs_format_only_in_the_bios_stub` holds that, against +//! `fatfs` itself. +//! +//! The boot sector carries no BIOS boot code: the jump fatgen103 requires is +//! there, and the 420 bytes after the BPB are zero. + +const SECTOR: usize = 512; +const RESERVED_SECTORS: u32 = 8; +const FATS: u32 = 2; +const FSINFO_SECTOR: usize = 1; +const BACKUP_BOOT_SECTOR: usize = 6; +const ROOT_CLUSTER: u32 = 2; +/// A fixed disk, which is what every FAT32 volume is. +const MEDIA: u8 = 0xF8; +/// The largest cluster number FAT32 can name, less the two reserved entries. +const MAX_FAT32_CLUSTERS: u32 = 0x0FFF_FFF4; +const END_OF_CHAIN: u32 = 0x0FFF_FFFF; +const ATTR_VOLUME_ID: u8 = 0x08; + +/// An empty FAT32 volume of `bytes` under `label`. +pub(crate) fn format(bytes: usize, label: [u8; 11]) -> Vec { + assert_eq!(bytes % SECTOR, 0, "a {bytes}-byte volume is not whole {SECTOR}-byte sectors"); + let total = u32::try_from(bytes / SECTOR) + .unwrap_or_else(|_| panic!("a {bytes}-byte volume has more sectors than FAT32 counts")); + let per_cluster = sectors_per_cluster(bytes as u64); + let per_fat = sectors_per_fat(total, per_cluster); + let first_data = RESERVED_SECTORS + FATS * per_fat; + let clusters = (total - first_data) / per_cluster; + assert!( + (toyos_fat32::MIN_FAT32_CLUSTERS..=MAX_FAT32_CLUSTERS).contains(&clusters), + "a {bytes}-byte volume has {clusters} clusters, which is no FAT32" + ); + + let mut volume = vec![0u8; bytes]; + let boot = boot_sector(total, per_cluster, per_fat, label); + volume[..SECTOR].copy_from_slice(&boot); + let backup = BACKUP_BOOT_SECTOR * SECTOR; + volume[backup..backup + SECTOR].copy_from_slice(&boot); + + let fsinfo = &mut volume[FSINFO_SECTOR * SECTOR..][..SECTOR]; + put(fsinfo, 0, &0x4161_5252u32.to_le_bytes()); + put(fsinfo, 484, &0x6141_7272u32.to_le_bytes()); + // Free count and next free: "unknown", which `toyos-fat32` counts and records. + put(fsinfo, 488, &u32::MAX.to_le_bytes()); + put(fsinfo, 492, &u32::MAX.to_le_bytes()); + put(fsinfo, 508, &0xAA55_0000u32.to_le_bytes()); + + let entries = per_fat as usize * SECTOR / 4; + for copy in 0..FATS { + let at = (RESERVED_SECTORS + copy * per_fat) as usize * SECTOR; + let fat = &mut volume[at..at + per_fat as usize * SECTOR]; + let mut set = |cluster: usize, value: u32| put(fat, cluster * 4, &value.to_le_bytes()); + set(0, 0x0FFF_FF00 | u32::from(MEDIA)); + set(1, u32::MAX); + set(ROOT_CLUSTER as usize, END_OF_CHAIN); + // The entries past the last cluster that the FAT's last sector still holds. + for cluster in clusters as usize + 2..entries { + set(cluster, END_OF_CHAIN); + } + } + + let root = first_data as usize * SECTOR; + put(&mut volume[root..], 0, &label); + volume[root + 11] = ATTR_VOLUME_ID; + volume +} + +/// 512 bytes up to 260 MiB and 4 KiB up to 8 GiB, past anything the build +/// formats. +fn sectors_per_cluster(bytes: u64) -> u32 { + const MIB: u64 = 1024 * 1024; + assert!(bytes <= 8 * 1024 * MIB, "a {bytes}-byte volume is larger than this format lays out"); + let cluster = if bytes <= 260 * MIB { 512 } else { 4096 }; + (cluster / SECTOR as u64) as u32 +} + +/// The smallest FAT whose entries cover every cluster the volume has left +/// once the reserved sectors and both FATs are taken out, the two reserved +/// entries included. +fn sectors_per_fat(total: u32, per_cluster: u32) -> u32 { + let rest = u64::from(total - RESERVED_SECTORS) + 2 * u64::from(per_cluster); + let per = u64::from(per_cluster * SECTOR as u32 * 8 / 32 + FATS); + rest.div_ceil(per) as u32 +} + +fn boot_sector(total: u32, per_cluster: u32, per_fat: u32, label: [u8; 11]) -> [u8; SECTOR] { + let mut s = [0u8; SECTOR]; + put(&mut s, 0, &[0xEB, 0x58, 0x90]); + put(&mut s, 3, b"MSWIN4.1"); + put(&mut s, 11, &(SECTOR as u16).to_le_bytes()); + s[13] = per_cluster as u8; + put(&mut s, 14, &(RESERVED_SECTORS as u16).to_le_bytes()); + s[16] = FATS as u8; + // The FAT12/16 root entry count and 16-bit sizes stay zero on FAT32, whose + // volumes `format` holds to more sectors than 16 bits count. + s[21] = MEDIA; + // Sectors per track and heads: no reader of a FAT32 volume uses either. + put(&mut s, 24, &0x20u16.to_le_bytes()); + put(&mut s, 26, &0x40u16.to_le_bytes()); + put(&mut s, 32, &total.to_le_bytes()); + put(&mut s, 36, &per_fat.to_le_bytes()); + put(&mut s, 44, &ROOT_CLUSTER.to_le_bytes()); + put(&mut s, 48, &(FSINFO_SECTOR as u16).to_le_bytes()); + put(&mut s, 50, &(BACKUP_BOOT_SECTOR as u16).to_le_bytes()); + s[64] = 0x80; + s[66] = 0x29; + put(&mut s, 67, &0x1234_5678u32.to_le_bytes()); + put(&mut s, 71, &label); + put(&mut s, 82, b"FAT32 "); + put(&mut s, 510, &[0x55, 0xAA]); + s +} + +fn put(into: &mut [u8], at: usize, bytes: &[u8]) { + into[at..at + bytes.len()].copy_from_slice(bytes); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + /// Where a FAT32 boot sector's boot code lies, after the BPB. + const BOOT_CODE: std::ops::Range = 90..510; + + /// Every size the build formats at, and the cluster sizes' edges. + const SIZES: [usize; 6] = [ + 34 << 20, + (34 << 20) + 4096, + 100 << 20, + 260 << 20, + (260 << 20) + 4096, + 300 << 20, + ]; + + fn fatfs_format(bytes: usize, label: [u8; 11]) -> Vec { + let mut volume = vec![0u8; bytes]; + fatfs::format_volume( + Cursor::new(&mut volume), + fatfs::FormatVolumeOptions::new().fat_type(fatfs::FatType::Fat32).volume_label(label), + ) + .expect("fatfs formats the volume"); + volume + } + + /// **The differential oracle**: `fatfs`, a third party's FAT + /// implementation sharing no code with this one, formats the same size + /// under the same label into the same bytes, but for the BIOS boot code + /// it writes into both copies of the boot sector and this module does not. + #[test] + fn differs_from_the_fatfs_format_only_in_the_bios_stub() { + for bytes in SIZES { + let ours = format(bytes, *b"TOYOS-SLOT "); + let theirs = fatfs_format(bytes, *b"TOYOS-SLOT "); + let mut masked = theirs.clone(); + for sector in [0, BACKUP_BOOT_SECTOR] { + let code = sector * SECTOR + BOOT_CODE.start..sector * SECTOR + BOOT_CODE.end; + assert!(ours[code.clone()].iter().all(|b| *b == 0), "{bytes}: boot code in sector {sector}"); + assert!(theirs[code.clone()].iter().any(|b| *b != 0), "{bytes}: fatfs wrote no boot code in sector {sector}"); + masked[code].fill(0); + } + let differing = (0..bytes).filter(|&at| ours[at] != masked[at]).count(); + assert!(differing == 0, "{bytes}: {differing} bytes differ from fatfs's"); + } + } + + /// `toyos-fat32` mounts the volume as empty, with every cluster but the + /// root's free, and once it has recorded that count, as the build has it + /// do, `toyos-fat32-check` finds nothing wrong with it. + #[test] + fn an_empty_volume_mounts_free_and_checks_clean() { + for bytes in SIZES { + let mut volume = format(bytes, *b"TOYOS-LOG "); + let mut fs = toyos_fat32::Fat32::mount(crate::image::VolumeIo(&mut volume)).expect("it mounts"); + assert!(fs.walk("", 8).expect("it walks").is_empty(), "{bytes}: an empty volume lists files"); + let geometry = *fs.geometry(); + let cluster = u64::from(geometry.bytes_per_cluster()); + assert_eq!(fs.free_bytes().expect("it counts"), (u64::from(geometry.cluster_count) - 1) * cluster, "{bytes}"); + fs.sync().expect("it records the count"); + drop(fs); + let complaints = toyos_fat32_check::check(&volume); + assert!(complaints.is_empty(), "{bytes}:\n{}", toyos_fat32_check::describe(&complaints)); + } + } +} diff --git a/src/flags.rs b/src/flags.rs index cf2a6b2259b..cf226d7f1aa 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -52,6 +52,9 @@ declare_flags!(pub CARGO_RUN = { pub HELP = "--help", None; pub CI = "--ci", Rest; pub CLIPPY = "--clippy", None; + /// Make the clang and `ld.lld` that run on ToyOS (`src/hostedclang.rs`), + /// and say where they are. + pub HOSTED_CLANG = "--hosted-clang", None; pub DEBUG = "--debug", None; pub BUILD_ONLY = "--build-only", None; pub DUMP_AUDIO = "--dump-audio", None; @@ -66,7 +69,6 @@ declare_flags!(pub CARGO_RUN = { pub BOOT_CONFIG = "--boot-config", Next; pub ARCH = "--arch", Next; pub REGEN_FONT = "--regen-font", None; - pub REGEN_WALLPAPER = "--regen-wallpaper", None; pub REGEN_SOUNDFONT = "--regen-soundfont", Next; /// Mint the owner's image-signing key where `signing::owner_key_path` /// says, refusing to replace one. diff --git a/src/gptwrite.rs b/src/gptwrite.rs new file mode 100644 index 00000000000..6f2e5e44f46 --- /dev/null +++ b/src/gptwrite.rs @@ -0,0 +1,237 @@ +//! The partition table the build writes onto every disk it makes: a +//! protective MBR, the primary GPT header and its entry array at the front, +//! and the backup array and header at the back, in 512-byte logical blocks +//! with the 128 entries of 128 bytes UEFI §5.3 requires room for. +//! +//! Here and not in `toyos-gpt`, which is the parser the kernel and the loader +//! read the table with; the build is the one program that writes one. +//! `toyos-gpt`'s CRC-32 and GUIDs are shared, because they are the format's +//! own and the parser is what proves them. `the_gpt_crate_writes_the_same_table` +//! holds every byte against the `gpt` crate this replaced. + +use toyos_gpt::Guid; + +const LBA: u64 = crate::image::LBA as u64; +const ENTRIES: u64 = 128; +const ENTRY_BYTES: u64 = 128; +const ARRAY_LBAS: u64 = ENTRIES * ENTRY_BYTES / LBA; +/// The MBR, the header and the array before it. +const FIRST_USABLE: u64 = 2 + ARRAY_LBAS; +const HEADER_BYTES: usize = 92; +/// A name is at most 36 UTF-16 code units. +const NAME_UNITS: usize = 36; + +/// One partition: its type, its own GUID, its first and last block, and its name. +pub(crate) struct Entry<'a> { + pub kind: Guid, + pub unique: Guid, + pub first_lba: u64, + pub last_lba: u64, + pub name: &'a str, +} + +/// A table's two halves: `primary` goes at byte 0 of the disk and `backup` +/// ends at its last byte. +pub(crate) struct Table { + pub primary: Vec, + pub backup: Vec, +} + +/// The last block a partition on a disk of `lbas` blocks may use. +fn last_usable(lbas: u64) -> u64 { + lbas - 2 - ARRAY_LBAS +} + +/// The table of a disk of `lbas` blocks named `disk`, carrying `entries` in +/// their order. +pub(crate) fn table(lbas: u64, disk: Guid, entries: &[Entry<'_>]) -> Table { + assert!(entries.len() as u64 <= ENTRIES, "{} partitions in a table of {ENTRIES}", entries.len()); + let last_usable = last_usable(lbas); + let mut array = vec![0u8; (ENTRIES * ENTRY_BYTES) as usize]; + for (entry, slot) in entries.iter().zip(array.chunks_mut(ENTRY_BYTES as usize)) { + assert!( + FIRST_USABLE <= entry.first_lba && entry.first_lba <= entry.last_lba && entry.last_lba <= last_usable, + "partition {:?} at blocks {}..={} is not inside {FIRST_USABLE}..={last_usable}", + entry.name, + entry.first_lba, + entry.last_lba + ); + let name: Vec = entry.name.encode_utf16().collect(); + assert!(name.len() <= NAME_UNITS, "the partition name {:?} is over {NAME_UNITS} UTF-16 units", entry.name); + slot[..16].copy_from_slice(&entry.kind.0); + slot[16..32].copy_from_slice(&entry.unique.0); + slot[32..40].copy_from_slice(&entry.first_lba.to_le_bytes()); + slot[40..48].copy_from_slice(&entry.last_lba.to_le_bytes()); + for (unit, at) in name.iter().zip((56..).step_by(2)) { + slot[at..at + 2].copy_from_slice(&unit.to_le_bytes()); + } + } + let array_crc = toyos_gpt::crc32(&array); + let header = |at: u64, other: u64, array_at: u64| { + let mut block = vec![0u8; LBA as usize]; + block[..8].copy_from_slice(b"EFI PART"); + block[8..12].copy_from_slice(&0x0001_0000u32.to_le_bytes()); + block[12..16].copy_from_slice(&(HEADER_BYTES as u32).to_le_bytes()); + block[24..32].copy_from_slice(&at.to_le_bytes()); + block[32..40].copy_from_slice(&other.to_le_bytes()); + block[40..48].copy_from_slice(&FIRST_USABLE.to_le_bytes()); + block[48..56].copy_from_slice(&last_usable.to_le_bytes()); + block[56..72].copy_from_slice(&disk.0); + block[72..80].copy_from_slice(&array_at.to_le_bytes()); + block[80..84].copy_from_slice(&(ENTRIES as u32).to_le_bytes()); + block[84..88].copy_from_slice(&(ENTRY_BYTES as u32).to_le_bytes()); + block[88..92].copy_from_slice(&array_crc.to_le_bytes()); + let crc = toyos_gpt::crc32(&block[..HEADER_BYTES]); + block[16..20].copy_from_slice(&crc.to_le_bytes()); + block + }; + + let mut primary = protective_mbr(lbas); + primary.extend(header(1, lbas - 1, 2)); + primary.extend(&array); + let mut backup = array; + backup.extend(header(lbas - 1, 1, last_usable + 1)); + Table { primary, backup } +} + +/// LBA 0: one partition of type 0xEE over the whole disk but its first +/// block, or as much of it as 32 bits count, at the CHS UEFI §5.2.3 gives it. +fn protective_mbr(lbas: u64) -> Vec { + let mut block = vec![0u8; LBA as usize]; + block[446..454].copy_from_slice(&[0x00, 0x00, 0x02, 0x00, 0xEE, 0xFF, 0xFF, 0xFF]); + block[454..458].copy_from_slice(&1u32.to_le_bytes()); + block[458..462].copy_from_slice(&u32::try_from(lbas - 1).unwrap_or(u32::MAX).to_le_bytes()); + block[510..512].copy_from_slice(&[0x55, 0xAA]); + block +} + +/// A version 4 GUID from the operating system's random source, in the +/// on-disk order: the version is the high nibble of byte 7, the third field +/// being little-endian, and the variant the top two bits of byte 8. +pub(crate) fn random_guid() -> Guid { + let mut bytes = [0u8; 16]; + getrandom::fill(&mut bytes).expect("the operating system's random source"); + bytes[7] = (bytes[7] & 0x0F) | 0x40; + bytes[8] = (bytes[8] & 0x3F) | 0x80; + Guid(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::image::ImageSectors; + use std::collections::BTreeMap; + use std::io::{Cursor, Read, Seek, SeekFrom}; + + const ALIGN: u64 = 2048; + + /// The partitions of a boot image with the DATA an install puts first, + /// one of them a length the 1 MiB alignment does not round. + const ROWS: [(&str, Guid, u64); 6] = [ + ("ToyOS data", Guid::TOYOS_DATA, 3 << 20), + ("ESP", Guid::EFI_SYSTEM, 34 << 20), + ("ToyOS slots", Guid::TOYOS_SLOTS, 1 << 20), + ("ToyOS log", Guid::MICROSOFT_BASIC, 34 << 20), + ("ToyOS slot A", Guid::TOYOS_BOOT, (40 << 20) + 4096), + ("ToyOS root A", Guid::TOYOS_ROOT, 7 << 20), + ]; + + /// `ROWS` under `uniques`, each on the first 1 MiB boundary after the last. + fn entries(uniques: &[Guid]) -> Vec> { + let mut at = ALIGN; + ROWS.iter() + .zip(uniques) + .map(|((name, kind, len), unique)| { + let first = at; + at = (first + len / LBA).next_multiple_of(ALIGN); + Entry { kind: *kind, unique: *unique, first_lba: first, last_lba: first + len / LBA - 1, name } + }) + .collect() + } + + fn disk_of(total: u64, table: &Table) -> Vec { + let mut disk = vec![0u8; total as usize]; + disk[..table.primary.len()].copy_from_slice(&table.primary); + let back = disk.len() - table.backup.len(); + disk[back..].copy_from_slice(&table.backup); + disk + } + + /// The `gpt` crate's table for `ROWS`, laid as `src/image.rs` laid it with + /// that crate, on a disk of `total` bytes: its bytes, its disk GUID and + /// the unique GUIDs it drew. + fn theirs(total: u64) -> (Vec, Guid, Vec) { + let mut bytes = vec![0u8; total as usize]; + let mut cursor = Cursor::new(&mut bytes); + gpt::mbr::ProtectiveMBR::with_lb_size(u32::try_from(total / LBA - 1).unwrap_or(u32::MAX)) + .overwrite_lba0(&mut cursor) + .expect("the protective MBR"); + let mut disk = gpt::GptConfig::default() + .initialized(false) + .writable(true) + .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) + .create_from_device(Box::new(cursor), None) + .expect("a table"); + disk.update_partitions(BTreeMap::new()).expect("an empty table"); + for (name, kind, len) in ROWS { + let kind = gpt::partition_types::Type { + guid: kind.to_string().leak(), + os: gpt::partition_types::OperatingSystem::None, + }; + disk.add_partition(name, len, kind, 0, Some(ALIGN)).expect("a partition"); + } + let mut device = disk.write().expect("the table"); + let mut out = vec![0u8; total as usize]; + device.seek(SeekFrom::Start(0)).and_then(|_| device.read_exact(&mut out)).expect("read back"); + let guid = |at: usize| Guid(out[at..at + 16].try_into().unwrap()); + let disk_guid = guid(LBA as usize + 56); + let uniques = (0..ROWS.len()).map(|i| guid(2 * LBA as usize + i * ENTRY_BYTES as usize + 16)).collect(); + (out, disk_guid, uniques) + } + + /// **The differential oracle**: the `gpt` crate, a third party's GPT + /// writer sharing no code with this one, lays the same partitions on the + /// same disk under the same GUIDs into the same bytes, every one of them, + /// on a disk the alignment rounds and on one it does not. + #[test] + fn the_gpt_crate_writes_the_same_table() { + for total in [128u64 << 20, (160 << 20) + 3 * 4096] { + let (theirs, disk, uniques) = theirs(total); + let ours = disk_of(total, &table(total / LBA, disk, &entries(&uniques))); + let differing = (0..total as usize).filter(|&i| ours[i] != theirs[i]).count(); + assert_eq!(differing, 0, "{total}: {differing} bytes differ from the gpt crate's"); + } + } + + /// RFC 9562's version and variant, where the canonical text shows them. + #[test] + fn a_random_guid_is_version_4_of_the_rfc_variant() { + let text = random_guid().to_string(); + assert_eq!(&text[14..15], "4", "{text}"); + assert!(matches!(&text[19..20], "8" | "9" | "A" | "B"), "{text}"); + } + + /// `toyos-gpt`, the parser the loader and the kernel read with, finds + /// every partition at its blocks under its type, from the primary and, + /// with the primary torn, from the backup. + #[test] + fn toyos_gpt_reads_it_from_either_copy() { + let total: u64 = 128 << 20; + let uniques: Vec = ROWS.iter().map(|_| random_guid()).collect(); + let entries = entries(&uniques); + let disk = disk_of(total, &table(total / LBA, random_guid(), &entries)); + let mut torn = disk.clone(); + torn[LBA as usize + 30] ^= 1; + for image in [&disk, &torn] { + for entry in &entries { + let found = toyos_gpt::locate(&mut ImageSectors(image), entry.unique).expect("the partition").partition(); + assert_eq!( + (found.type_guid(), found.first_lba(), found.lba_count().get()), + (entry.kind, entry.first_lba, entry.last_lba - entry.first_lba + 1), + "{}", + entry.name + ); + } + } + } +} diff --git a/src/hostedclang.rs b/src/hostedclang.rs new file mode 100644 index 00000000000..a4480cf4b6d --- /dev/null +++ b/src/hostedclang.rs @@ -0,0 +1,324 @@ +//! clang and `ld.lld` for a ToyOS host: built for `x86_64-unknown-toyos` from +//! the LLVM commit the host's LLVM is (`src/llvm.rs`), by the toolchain's own +//! clang against its C sysroot (`clang::CSysroot`), and linked statically. +//! CMake builds them, beside bootstrap, until bootstrap's LLVM for a ToyOS +//! host makes both +//! (`issues/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`). +//! +//! **Made only when asked for** (`cargo run -- --hosted-clang`): its build is +//! LLVM's, and its key moves with every sysroot's, so no other build makes it. +//! +//! **A function of its key** ([`key_of`]): the host LLVM's key, which names the +//! commit, what of it the LLVM keeps, the revision and repository it says it +//! was built from, and the host's tools, n2 and CMake among them; the +//! sysroot's, which names the C library, the C++ runtime and the clang that +//! builds against them; and [`RECIPE`] with [`OPTIONS`]. Its sources are +//! [`SOURCES`] of the commit as that LLVM keeps them in the store +//! (`src/llvm.rs`), never a checkout's files. CMake builds LLVM's tablegens for +//! the build machine first, in a nested build of its own (`NATIVE`), with the C +//! and C++ compilers the LLVM key names. +//! +//! `hosted-clang//` in the store holds `bin/clang`, `bin/ld.lld` and +//! clang's resource headers in `lib/clang//include`, where clang looks +//! beside itself; once its [`SOURCE`] file exists it is read-only. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use crate::arch::Arch; +use crate::buildlock::{Guard, Keyed}; +use crate::clang::CSysroot; +use crate::keystore::{self, Key}; +use crate::sysroot::clone_tree; + +/// What changes how the key's sources become this product and is none of the +/// other parts. Moving it moves every key. +const RECIPE: &str = "CMake and n2 build of clang and lld from the SOURCES the host's LLVM keeps, for the TARGET \ + against its C sysroot, saying the version, revision and repository the host's clang \ + says; of the build, clang as bin/clang, lld as bin/ld.lld and clang's resource \ + headers; 3"; + +/// The ToyOS the binaries run on. +const TARGET: Arch = Arch::X86_64; + +/// What of the commit the build reads, as git's pathspecs the host's LLVM keeps: +/// LLVM, clang and LLD, the CMake modules they share, the third-party sources +/// LLVM compiles in, and libunwind's headers, which LLD's Mach-O port reads. No +/// test, which the configuration builds none of. +pub(crate) const SOURCES: [&str; 12] = [ + "llvm", + "clang", + "lld", + "cmake", + "third-party", + "libunwind/include", + ":(exclude)llvm/test", + ":(exclude)llvm/unittests", + ":(exclude)clang/test", + ":(exclude)clang/unittests", + ":(exclude)lld/test", + ":(exclude)lld/unittests", +]; + +/// The CMake options beyond the compilers', the sysroot's and the host +/// libraries [`crate::llvm`] turns off, each with its reason. +const OPTIONS: [(&str, &str); 11] = [ + ("CMAKE_BUILD_TYPE", "Release"), + ("LLVM_ENABLE_PROJECTS", "clang;lld"), + // The two architectures ToyOS runs on, as the host's LLVM targets them + // (`clang::LLVM_CONFIG`). + ("LLVM_TARGETS_TO_BUILD", "AArch64;X86"), + // What the binaries run on and compile for, where LLVM otherwise asks + // `config.guess` about the build machine. + ("LLVM_HOST_TRIPLE", TARGET.userland()), + ("LLVM_DEFAULT_TARGET_TRIPLE", TARGET.userland()), + ("LLVM_INCLUDE_TESTS", "OFF"), + ("LLVM_INCLUDE_BENCHMARKS", "OFF"), + ("LLVM_INCLUDE_EXAMPLES", "OFF"), + ("LLVM_INCLUDE_DOCS", "OFF"), + ("CLANG_INCLUDE_TESTS", "OFF"), + ("CLANG_INCLUDE_DOCS", "OFF"), +]; + +/// The file a finished product carries last, naming its key. +const SOURCE: &str = "SOURCE"; + +/// The binaries it keeps, each as the build names it and as it is kept: lld +/// takes its flavour from the name it is run by. +const BINARIES: [(&str, &str); 2] = [("clang", "clang"), ("lld", "ld.lld")]; + +/// A ToyOS-hosted clang and LLD, held in use for as long as this lives. +pub struct HostedClang { + pub dir: PathBuf, + _using: Guard, +} + +/// `cargo run -- --hosted-clang`: make this tree's, and name each binary with +/// its size. +pub fn dispatch(root: &Path) { + let mut lock = crate::buildlock::shared(root, "the ToyOS-hosted clang"); + let sysroot = crate::toolchain::ensure(root, &mut lock); + let fork = crate::sysroot::fork_checkout(root, &mut lock); + let store = keystore::host(); + let llvm = crate::llvm::resolve_held(root, &store, &fork, &mut lock); + let hosted = resolve(&store, &fork, &llvm.dir, sysroot.dir(), &crate::n2::ninja(root)); + for (_, kept) in BINARIES { + let binary = hosted.dir.join("bin").join(kept); + let size = fs::metadata(&binary).unwrap_or_else(|e| panic!("stat {}: {e}", binary.display())).len(); + println!("{} ({size} bytes)", binary.display()); + } +} + +/// The product of the LLVM at `llvm`, the one the fork `fork` names, and the +/// sysroot at `sysroot`, both held in use in `store`: made if nobody on this +/// host has made it, under `ninja`. +pub fn resolve(store: &Path, fork: &Path, llvm: &Path, sysroot: &Path, ninja: &Path) -> HostedClang { + let key = key_of(&stored(llvm), &stored(sysroot), &options()); + let dir = Keyed::HostedClang.store(store).join(&key); + let make = || place(fork, llvm, sysroot, ninja, &key, &dir); + let using = keystore::made(store, Keyed::HostedClang, &key, || defect(&dir), make); + HostedClang { dir, _using: using } +} + +/// The key the store product at `dir` is filed under. +fn stored(dir: &Path) -> Key { + let named = dir.file_name().and_then(|n| n.to_str()).and_then(Key::parse); + named.unwrap_or_else(|| panic!("{} is no product of the store: its name is no key", dir.display())) +} + +fn key_of(llvm: &Key, sysroot: &Key, options: &[(String, String)]) -> Key { + let options: Vec = options.iter().map(|(name, value)| format!("{name}={value}")).collect(); + let parts = [RECIPE, TARGET.userland(), &options.join("\n"), llvm.as_str(), sysroot.as_str()]; + Key::of(parts.join("\n\0\n").as_bytes()) +} + +/// [`OPTIONS`], and every host library off. +fn options() -> Vec<(String, String)> { + let off = crate::llvm::NO_HOST_LIBRARIES.iter().map(|name| (*name, "OFF")); + OPTIONS.into_iter().chain(off).map(|(name, value)| (name.to_string(), value.to_string())).collect() +} + +/// Why `dir` is not a finished product, if it is not. +fn defect(dir: &Path) -> Option { + if !dir.join(SOURCE).is_file() { + return Some(format!("{} carries no {SOURCE}", dir.display())); + } + let binaries = BINARIES.iter().map(|(_, kept)| dir.join("bin").join(kept)); + let mut gone: Vec = binaries.filter(|p| !p.is_file()).map(|p| p.display().to_string()).collect(); + let headers = fs::read_dir(dir.join("lib/clang")).ok().and_then(|mut d| d.next()).and_then(Result::ok); + if !headers.is_some_and(|version| version.path().join("include/stddef.h").is_file()) { + gone.push(dir.join("lib/clang//include").display().to_string()); + } + (!gone.is_empty()).then(|| format!("{} carries no {}", dir.display(), gone.join(", "))) +} + +/// Build what `key` names and put it at `dir`. The caller holds the key's lock. +fn place(fork: &Path, llvm: &Path, sysroot: &Path, ninja: &Path, key: &Key, dir: &Path) { + eprintln!("Building the ToyOS-hosted clang and LLD {key}: nobody on this host has"); + let scratch = dir.with_extension("build"); + keystore::remove(&scratch); + let built = scratch.join("build"); + build(fork, &llvm.join("src"), &built, &CSysroot::of(sysroot, TARGET), ninja, &scratch); + + let partial = dir.with_extension("partial"); + keystore::remove(&partial); + let bin = partial.join("bin"); + fs::create_dir_all(&bin).unwrap_or_else(|e| panic!("create {}: {e}", bin.display())); + for (name, kept) in BINARIES { + // `fs::copy` follows the link the build installs clang as. + let (from, to) = (built.join("bin").join(name), bin.join(kept)); + fs::copy(&from, &to).unwrap_or_else(|e| panic!("copy {} -> {}: {e}", from.display(), to.display())); + } + let resource = crate::clang::resource_version(&built); + let version = resource.file_name().unwrap_or_else(|| panic!("{} names no version", resource.display())); + clone_tree(&resource.join("include"), &partial.join("lib/clang").join(version).join("include")); + // The fork the version stamp was read from names the LLVM built, or this + // is not the key's. + let again = crate::llvm::key(fork); + assert!( + again == stored(llvm), + "the fork moved while {key} was being built (it now names LLVM {again}); nothing was kept" + ); + let source = partial.join(SOURCE); + fs::write(&source, format!("{key}\n")).unwrap_or_else(|e| panic!("write {}: {e}", source.display())); + crate::llvm::read_only(&partial); + keystore::retire(dir); + fs::rename(&partial, dir).unwrap_or_else(|e| panic!("rename {} -> {}: {e}", partial.display(), dir.display())); + keystore::remove(&scratch); +} + +/// Configure LLVM at `sources`, of the commit `fork` names, for `c`'s target, +/// in `built`, and build clang and LLD there under `ninja`; `scratch` holds +/// the links CMake runs the compilers by. +fn build(fork: &Path, sources: &Path, built: &Path, c: &CSysroot, ninja: &Path, scratch: &Path) { + // The driver links C++ when run as `clang++`, and LLVM's archiver indexes + // as `ranlib` when run by that name. + let links = scratch.join("bin"); + fs::create_dir_all(&links).unwrap_or_else(|e| panic!("create {}: {e}", links.display())); + let (cxx, ranlib) = (links.join("clang++"), links.join("llvm-ranlib")); + for (link, to) in [(&cxx, &c.clang), (&ranlib, &c.ar)] { + std::os::unix::fs::symlink(to, link) + .unwrap_or_else(|e| panic!("symlink {} -> {}: {e}", link.display(), to.display())); + } + let path = |p: &Path| p.display().to_string(); + let (cc, cxx_native) = crate::llvm::host_compilers(); + let mut native = vec![format!("-DCMAKE_C_COMPILER={}", path(&cc)), format!("-DCMAKE_CXX_COMPILER={}", path(&cxx_native))]; + native.extend(crate::llvm::NO_HOST_LIBRARIES.iter().map(|name| format!("-D{name}=OFF"))); + let mut definitions = options(); + definitions.extend(crate::llvm::stamp_options(fork)); + definitions.push(("LLVM_VERSION_SUFFIX".to_string(), version_suffix(&c.clang))); + definitions.extend([ + ("CMAKE_TOOLCHAIN_FILE".to_string(), path(&c.cmake_toolchain())), + ("CMAKE_C_COMPILER".to_string(), path(&c.clang)), + ("CMAKE_ASM_COMPILER".to_string(), path(&c.clang)), + ("CMAKE_CXX_COMPILER".to_string(), path(&cxx)), + ("CMAKE_AR".to_string(), path(&c.ar)), + ("CMAKE_RANLIB".to_string(), path(&ranlib)), + ("CMAKE_MAKE_PROGRAM".to_string(), path(ninja)), + ("CROSS_TOOLCHAIN_FLAGS_NATIVE".to_string(), native.join(";")), + ]); + let mut configure = Command::new("cmake"); + configure.args(["-G", "Ninja", "-Wno-dev", "-S"]).arg(sources.join("llvm")).arg("-B").arg(built); + configure.args(definitions.iter().map(|(name, value)| format!("-D{name}={value}"))); + run(configure, "configuration", ninja); + let mut make = Command::new(ninja); + make.arg("-C").arg(built).args(BINARIES.map(|(name, _)| name)); + run(make, "build", ninja); +} + +/// What the host's clang, the sysroot's, says it is beyond LLVM's version: +/// bootstrap's `LLVM_VERSION_SUFFIX`. Every object a clang compiles carries +/// what it says in its `.comment`, so this one says it too. +fn version_suffix(clang: &Path) -> String { + let mut command = Command::new(clang); + command.arg("--version"); + crate::llvm::clear(&mut command); + let out = command.output().unwrap_or_else(|e| panic!("run {command:?}: {e}")); + assert!(out.status.success(), "{command:?} failed: {}", String::from_utf8_lossy(&out.stderr)); + let said = String::from_utf8_lossy(&out.stdout); + let first = said.lines().next().unwrap_or_default(); + suffix(first).unwrap_or_else(|| panic!("{} says no version: {said}", clang.display())) +} + +/// The suffix of the version `line`, clang's first line of `--version`, names: +/// what follows its first `-`, and none without one. +fn suffix(line: &str) -> Option { + let version = line.strip_prefix("clang version ")?.split_whitespace().next()?; + Some(version.find('-').map_or(String::new(), |at| version[at..].to_string())) +} + +/// Run `command` seeing nothing of this process's environment but what the +/// LLVM build does (`llvm::clear`), with `ninja`'s directory first on `PATH` +/// for the nested build CMake runs, and refuse its failure. +fn run(mut command: Command, what: &str, ninja: &Path) { + crate::llvm::clear(&mut command); + let dir = ninja.parent().unwrap_or_else(|| panic!("{} is in no directory", ninja.display())); + let caller = std::env::var_os("PATH").unwrap_or_default(); + let path = std::env::join_paths(std::iter::once(dir.to_path_buf()).chain(std::env::split_paths(&caller))) + .unwrap_or_else(|e| panic!("{} cannot lead PATH: {e}", dir.display())); + command.env("PATH", path); + let status = command.status().unwrap_or_else(|e| panic!("run {command:?}: {e}")); + assert!(status.success(), "the ToyOS-hosted clang's {what} failed ({status}): its output above says why"); +} + +#[cfg(test)] +mod tests { + use toyos_tmpdir::TempDir; + + use super::*; + + fn key(text: &str) -> Key { + Key::of(text.as_bytes()) + } + + /// **The key moves with each input and with nothing else**: the host + /// LLVM's key, the sysroot's and every CMake option. + #[test] + fn the_key_moves_with_the_llvm_the_sysroot_and_the_options() { + let (llvm, sysroot) = (key("llvm"), key("sysroot")); + let base = key_of(&llvm, &sysroot, &options()); + assert_eq!(key_of(&llvm, &sysroot, &options()), base); + let mut moved = options(); + moved[0].1 = "Debug".to_string(); + for (what, other) in [ + ("the LLVM", key_of(&key("another llvm"), &sysroot, &options())), + ("the sysroot", key_of(&llvm, &key("another sysroot"), &options())), + ("an option", key_of(&llvm, &sysroot, &moved)), + ] { + assert_ne!(other, base, "{what} did not move the key"); + } + } + + /// **The suffix is what follows the version's first `-`**, none without + /// one, and a line that names no clang version names none. + #[test] + fn the_suffix_is_what_follows_the_version() { + let rev = "(https://github.com/ToyOSOrg/llvm-project.git ceaf0fbb8440)"; + assert_eq!(suffix(&format!("clang version 22.1.8-rust-dev {rev}")).as_deref(), Some("-rust-dev")); + assert_eq!(suffix(&format!("clang version 22.1.8 {rev}")).as_deref(), Some("")); + assert_eq!(suffix("Apple clang version 17.0.0 (clang-1700.0.13.5)"), None); + } + + /// **A product missing a binary or clang's headers is not whole**, nor one + /// without its `SOURCE`. + #[test] + fn a_product_without_a_binary_or_its_headers_is_not_whole() { + let dir = TempDir::new("hosted-clang"); + let write = |rel: &str| { + let path = dir.join(rel); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, rel).unwrap(); + }; + assert!(defect(&dir).is_some_and(|d| d.contains(SOURCE))); + for rel in [SOURCE, "bin/clang", "bin/ld.lld", "lib/clang/22/include/stddef.h"] { + write(rel); + } + assert_eq!(defect(&dir), None); + for gone in ["bin/ld.lld", "lib/clang/22/include/stddef.h"] { + fs::remove_file(dir.join(gone)).unwrap(); + assert!(defect(&dir).is_some_and(|d| d.contains(gone.split("22").next().unwrap())), "{gone}: {:?}", defect(&dir)); + write(gone); + } + } +} diff --git a/src/icons.rs b/src/icons.rs new file mode 100644 index 00000000000..621f3bb8fa1 --- /dev/null +++ b/src/icons.rs @@ -0,0 +1,827 @@ +//! The desktop's icons, drawn at build time: every `icons/.svg` an image +//! carries ships as `share/icons/.alpha`, its coverage at the one size +//! [`SIZES`] gives it, and the program that draws it only colours that mask — +//! no program carries a rasterizer, and none needs one while no icon is drawn +//! at a size the build did not know. +//! +//! An `.alpha` file is the width and height as little-endian `u32`, then one +//! byte of coverage per pixel, row by row. +//! +//! The SVG is read as the subset the icons are written in, and anything past it +//! is refused by name: a root `svg` with a square `viewBox`, then `path`, +//! `line`, `circle` and `rect`, each painted `currentColor` or `none`. A literal +//! colour is refused because the mask cannot carry it; an attribute this does +//! not read is refused because it would ship drawn without it. A stroke must be +//! round-capped and round-joined, which makes it exactly the points within half +//! its width of its path, the region drawn here. +//! +//! The pixels follow resvg's, which drew these icons before the build did, +//! because the desktop's look is not this file's to change: a pixel +//! samples four rows of four points, a sample worth 16 of 255; a fill is +//! nonzero, its arcs become cubics as `kurbo` splits them and each cubic, cut +//! where it turns in y, becomes `2^shift` chords by tiny-skia's count; and each +//! shape is laid over the ones before it. + +/// Each icon and the edge, in pixels, it is drawn at: the compositor's cursors +/// at 20, its title-bar buttons at 14, and `files`' entries at 32. +const SIZES: &[(&str, u32)] = &[ + ("cursor-bold", 20), + ("arrow-down-right-bold", 20), + ("crosshair-simple-bold", 20), + ("minus-bold", 14), + ("square-bold", 14), + ("x-bold", 14), + ("folder-bold", 32), + ("file-bold", 32), +]; + +/// Sample points per pixel along each axis. +const GRID: usize = 4; + +type Point = (f64, f64); + +/// The `.alpha` file the icon `stem` ships as. +pub fn rasterize(stem: &str, svg: &[u8]) -> Vec { + let size = SIZES + .iter() + .find(|(name, _)| *name == stem) + .unwrap_or_else(|| panic!("icons/{stem}.svg has no size in src/icons.rs's SIZES")) + .1; + let svg = std::str::from_utf8(svg).unwrap_or_else(|e| panic!("icons/{stem}.svg: {e}")); + let mut out = Vec::with_capacity(8 + (size * size) as usize); + out.extend(size.to_le_bytes()); + out.extend(size.to_le_bytes()); + out.extend(coverage(&Icon::parse(svg), size as usize)); + out +} + +/// A subpath: its start, then one cubic per segment, a line as the cubic +/// that runs along it. +#[derive(Clone)] +struct Subpath { + start: Point, + cubics: Vec<[Point; 3]>, + closed: bool, +} + +struct Shape { + subpaths: Vec, + fill: bool, + /// Half the stroke's width; `None` where it is not stroked. + stroke: Option, +} + +struct Icon { + /// The `viewBox`'s origin and edge, in user units. + origin: Point, + edge: f64, + shapes: Vec, +} + +impl Icon { + fn parse(svg: &str) -> Icon { + let mut tags = Tags { rest: svg.trim() }; + let (name, attrs, open) = tags.next().expect("an empty SVG"); + assert!(name == "svg" && open, "the root element is <{name}>, not an open "); + let mut root_fill = None; + let mut view_box = None; + for (key, value) in attrs { + match key { + "xmlns" => assert_eq!(value, "http://www.w3.org/2000/svg", "xmlns"), + "viewBox" => view_box = Some(numbers(value)), + "fill" => root_fill = Some(paint(value)), + _ => panic!(" is not read"), + } + } + let view_box = view_box.expect("an with no viewBox"); + let [x, y, w, h] = view_box[..] else { panic!("viewBox {view_box:?} is not four numbers") }; + assert!(w == h && w > 0.0, "viewBox {view_box:?} is not square"); + + let mut shapes = vec![]; + loop { + let (name, attrs, open) = tags.next().expect("an that never closes"); + if name == "/svg" { + break; + } + assert!(!open, "<{name}> has children"); + shapes.push(Shape::parse(name, &attrs, root_fill)); + } + assert!(tags.rest.trim().is_empty(), "text after : {:?}", tags.rest); + Icon { origin: (x, y), edge: w, shapes } + } +} + +impl Shape { + fn parse(name: &str, attrs: &[(&str, &str)], root_fill: Option) -> Shape { + let mut fill = root_fill; + let mut stroke = false; + let mut width = 1.0; + let mut round = (false, false); + let mut path = Path::default(); + let num = |value: &str| -> f64 { value.parse().unwrap_or_else(|_| panic!("<{name}> {value:?} is not a number")) }; + let get = |key: &str| attrs.iter().find(|(k, _)| *k == key).map(|(_, v)| num(v)); + let geometry: &[&str] = match name { + "path" => { + let d = attrs.iter().find(|(k, _)| *k == "d").expect("a with no d").1; + path.data(d); + &["d"] + } + "line" => { + let at = |k| get(k).unwrap_or(0.0); + path.move_to((at("x1"), at("y1"))); + path.line_to((at("x2"), at("y2"))); + &["x1", "y1", "x2", "y2"] + } + "circle" => { + let (x, y, r) = (get("cx").unwrap_or(0.0), get("cy").unwrap_or(0.0), get("r").expect("r")); + path.move_to((x + r, y)); + for to in [(x, y + r), (x - r, y), (x, y - r), (x + r, y)] { + path.arc(r, r, 0.0, false, true, to); + } + path.close(); + &["cx", "cy", "r"] + } + "rect" => { + let (x, y) = (get("x").unwrap_or(0.0), get("y").unwrap_or(0.0)); + let (w, h) = (get("width").expect("width"), get("height").expect("height")); + let (rx, ry) = match (get("rx"), get("ry")) { + (None, None) => (0.0, 0.0), + (Some(r), None) | (None, Some(r)) => (r, r), + (Some(rx), Some(ry)) => (rx, ry), + }; + let (rx, ry) = (rx.min(w / 2.0), ry.min(h / 2.0)); + path.move_to((x + rx, y)); + path.line_to((x + w - rx, y)); + path.arc(rx, ry, 0.0, false, true, (x + w, y + ry)); + path.line_to((x + w, y + h - ry)); + path.arc(rx, ry, 0.0, false, true, (x + w - rx, y + h)); + path.line_to((x + rx, y + h)); + path.arc(rx, ry, 0.0, false, true, (x, y + h - ry)); + path.line_to((x, y + ry)); + path.arc(rx, ry, 0.0, false, true, (x + rx, y)); + path.close(); + &["x", "y", "width", "height", "rx", "ry"] + } + _ => panic!("<{name}> is not drawn"), + }; + for &(key, value) in attrs { + match key { + "fill" => fill = Some(paint(value)), + "stroke" => stroke = paint(value), + "stroke-width" => width = num(value), + "stroke-linecap" => round.0 = value == "round", + "stroke-linejoin" => round.1 = value == "round", + _ if geometry.contains(&key) => {} + _ => panic!("<{name} {key}> is not read"), + } + } + let fill = fill.unwrap_or_else(|| panic!("<{name}> fills black, a colour no mask carries")); + assert!(!stroke || round == (true, true), "<{name}> is stroked without round caps and joins"); + Shape { subpaths: path.finish(), fill, stroke: stroke.then_some(width / 2.0) } + } +} + +/// Whether a paint draws: `currentColor` does, `none` does not. +fn paint(value: &str) -> bool { + match value { + "currentColor" => true, + "none" => false, + _ => panic!("paint {value:?} is neither currentColor nor none"), + } +} + +fn numbers(value: &str) -> Vec { + let mut data = Data { rest: value.as_bytes() }; + std::iter::from_fn(|| data.number()).collect() +} + +/// An SVG element: its name, its attributes and whether it opens. +type Tag<'a> = (&'a str, Vec<(&'a str, &'a str)>, bool); + +/// An SVG's elements, one [`Tag`] at a time. +struct Tags<'a> { + rest: &'a str, +} + +impl<'a> Tags<'a> { + fn next(&mut self) -> Option> { + self.rest = self.rest.trim_start(); + if self.rest.is_empty() { + return None; + } + let end = self.rest.find('>').unwrap_or_else(|| panic!("an unclosed tag: {:?}", self.rest)); + let tag = self.rest[..end].strip_prefix('<').unwrap_or_else(|| panic!("text where a tag goes: {:?}", self.rest)); + self.rest = &self.rest[end + 1..]; + let (tag, open) = match tag.strip_suffix('/') { + Some(tag) => (tag, false), + None => (tag, !tag.starts_with('/')), + }; + let tag = tag.trim(); + let (name, mut attrs) = tag.split_once(char::is_whitespace).unwrap_or((tag, "")); + let mut out = vec![]; + loop { + attrs = attrs.trim_start(); + if attrs.is_empty() { + break; + } + let (key, rest) = attrs.split_once("=\"").unwrap_or_else(|| panic!("<{name}>: {attrs:?}")); + let (value, rest) = rest.split_once('"').unwrap_or_else(|| panic!("<{name}>: {attrs:?}")); + out.push((key.trim(), value)); + attrs = rest; + } + Some((name, out, open)) + } +} + +/// Path data, read one number or command at a time. +struct Data<'a> { + rest: &'a [u8], +} + +impl Data<'_> { + fn skip(&mut self) { + while let [b' ' | b'\t' | b'\n' | b'\r' | b',', rest @ ..] = self.rest { + self.rest = rest; + } + } + + fn command(&mut self) -> Option { + self.skip(); + match self.rest { + [c, rest @ ..] if c.is_ascii_alphabetic() => { + self.rest = rest; + Some(*c) + } + _ => None, + } + } + + fn number(&mut self) -> Option { + self.skip(); + let mut end = 0; + let digits = |at: &mut usize, rest: &[u8]| { + while rest.get(*at).is_some_and(u8::is_ascii_digit) { + *at += 1; + } + }; + if matches!(self.rest.first(), Some(b'+' | b'-')) { + end += 1; + } + digits(&mut end, self.rest); + if self.rest.get(end) == Some(&b'.') { + end += 1; + digits(&mut end, self.rest); + } + if matches!(self.rest.get(end), Some(b'e' | b'E')) { + end += 1; + if matches!(self.rest.get(end), Some(b'+' | b'-')) { + end += 1; + } + digits(&mut end, self.rest); + } + if end == 0 { + return None; + } + let text = std::str::from_utf8(&self.rest[..end]).unwrap(); + self.rest = &self.rest[end..]; + Some(text.parse().unwrap_or_else(|_| panic!("{text:?} is not a number"))) + } + + fn flag(&mut self) -> bool { + self.skip(); + match self.rest { + [c @ (b'0' | b'1'), rest @ ..] => { + self.rest = rest; + *c == b'1' + } + _ => panic!("an arc flag that is not 0 or 1"), + } + } + + fn point(&mut self) -> Point { + let x = self.number().expect("a coordinate"); + (x, self.number().expect("a coordinate")) + } +} + +/// A shape's subpaths as they are read. +#[derive(Default)] +struct Path { + done: Vec, + open: Option, +} + +impl Path { + fn at(&self) -> Point { + let open = self.open.as_ref().expect("a drawing command before any moveto"); + open.cubics.last().map_or(open.start, |c| c[2]) + } + + /// Ends the current subpath; a lone moveto draws nothing. + fn end(&mut self, closed: bool) { + if let Some(open) = self.open.take().filter(|open| !open.cubics.is_empty()) { + self.done.push(Subpath { closed, ..open }); + } + } + + fn finish(mut self) -> Vec { + self.end(false); + self.done + } + + fn move_to(&mut self, p: Point) { + self.end(false); + self.open = Some(Subpath { start: p, cubics: vec![], closed: false }); + } + + fn cubic_to(&mut self, c1: Point, c2: Point, p: Point) { + self.at(); + self.open.as_mut().unwrap().cubics.push([c1, c2, p]); + } + + fn line_to(&mut self, p: Point) { + let a = self.at(); + let third = |t: f64| (a.0 + (p.0 - a.0) * t, a.1 + (p.1 - a.1) * t); + self.cubic_to(third(1.0 / 3.0), third(2.0 / 3.0), p); + } + + fn close(&mut self) { + let start = self.open.as_ref().expect("a closepath before any moveto").start; + self.end(true); + self.open = Some(Subpath { start, cubics: vec![], closed: false }); + } + + /// An elliptical arc: its centre by the SVG specification's conversion + /// (SVG 1.1, appendix F.6.5 and F.6.6), then the cubics `kurbo` splits it + /// into at the 0.1 user units usvg asks of it. + fn arc(&mut self, rx: f64, ry: f64, degrees: f64, large: bool, sweep: bool, p: Point) { + let p0 = self.at(); + let (mut rx, mut ry) = (rx.abs(), ry.abs()); + if p0 == p { + return; + } + if rx <= 1e-5 || ry <= 1e-5 { + return self.line_to(p); + } + let (sin, cos) = degrees.to_radians().sin_cos(); + let (hx, hy) = ((p0.0 - p.0) / 2.0, (p0.1 - p.1) / 2.0); + let (x1, y1) = (cos * hx + sin * hy, -sin * hx + cos * hy); + let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry); + if lambda > 1.0 { + rx *= lambda.sqrt(); + ry *= lambda.sqrt(); + } + let num = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1; + let den = rx * rx * y1 * y1 + ry * ry * x1 * x1; + let k = (num / den).abs().sqrt() * if large == sweep { -1.0 } else { 1.0 }; + let (cx1, cy1) = (k * rx * y1 / ry, -k * ry * x1 / rx); + let centre = (cos * cx1 - sin * cy1 + f64::midpoint(p0.0, p.0), sin * cx1 + cos * cy1 + f64::midpoint(p0.1, p.1)); + let start = ((y1 - cy1) / ry).atan2((x1 - cx1) / rx); + let tau = std::f64::consts::TAU; + let mut delta = (((-y1 - cy1) / ry).atan2((-x1 - cx1) / rx) - start) % tau; + if sweep && delta < 0.0 { + delta += tau; + } else if !sweep && delta > 0.0 { + delta -= tau; + } + + let n = ((1.1163 * rx.max(ry) / 0.1).powf(1.0 / 6.0).max(3.999_999) * delta.abs() / tau).ceil(); + let step = delta / n; + let arm = 4.0 / 3.0 * (step / 4.0).tan(); + let at = |angle: f64| { + let (s, c) = angle.sin_cos(); + (rx * cos * c - ry * sin * s, rx * sin * c + ry * cos * s) + }; + for i in 0..n as usize { + let (a0, a1) = (start + step * i as f64, start + step * (i + 1) as f64); + let (q0, d0) = (at(a0), at(a0 + tau / 4.0)); + let (q1, d1) = (at(a1), at(a1 + tau / 4.0)); + let c1 = (centre.0 + q0.0 + arm * d0.0, centre.1 + q0.1 + arm * d0.1); + let c2 = (centre.0 + q1.0 - arm * d1.0, centre.1 + q1.1 - arm * d1.1); + let end = if i + 1 == n as usize { p } else { (centre.0 + q1.0, centre.1 + q1.1) }; + self.cubic_to(c1, c2, end); + } + } + + fn data(&mut self, d: &str) { + let mut data = Data { rest: d.as_bytes() }; + let mut command = data.command().expect("path data that does not start with a command"); + loop { + let at = self.open.as_ref().map_or((0.0, 0.0), |_| self.at()); + let relative = command.is_ascii_lowercase(); + let abs = |p: Point| if relative { (at.0 + p.0, at.1 + p.1) } else { p }; + match command.to_ascii_uppercase() { + b'M' => { + // A relative one after a closepath is from the closed + // subpath's start, which `close` leaves as the current point. + self.move_to(abs(data.point())); + // Coordinates after a moveto are linetos. + command = if relative { b'l' } else { b'L' }; + } + b'L' => self.line_to(abs(data.point())), + b'H' => { + let x = data.number().expect("an x"); + self.line_to((if relative { at.0 + x } else { x }, at.1)); + } + b'V' => { + let y = data.number().expect("a y"); + self.line_to((at.0, if relative { at.1 + y } else { y })); + } + b'C' => { + let (c1, c2) = (abs(data.point()), abs(data.point())); + let p = abs(data.point()); + self.cubic_to(c1, c2, p); + } + b'A' => { + let (rx, ry) = (data.number().expect("rx"), data.number().expect("ry")); + let degrees = data.number().expect("an x-axis rotation"); + let (large, sweep) = (data.flag(), data.flag()); + let p = abs(data.point()); + self.arc(rx, ry, degrees, large, sweep, p); + } + b'Z' => self.close(), + _ => panic!("path command {:?} is not read", command as char), + } + if let Some(next) = data.command() { + command = next; + } else if data.rest.is_empty() { + break; + } else if command.eq_ignore_ascii_case(&b'Z') { + panic!("a number after a closepath"); + } + } + } +} + +impl Subpath { + fn map(&self, f: impl Fn(Point) -> Point) -> Subpath { + Subpath { + start: f(self.start), + cubics: self.cubics.iter().map(|c| c.map(&f)).collect(), + closed: self.closed, + } + } + + /// The subpath as chords, each cubic cut into as many as `chords` says. + fn flatten(&self, chords: impl Fn(&[Point; 4]) -> Vec) -> Vec { + let mut points = vec![self.start]; + for c in &self.cubics { + let p0 = *points.last().unwrap(); + points.extend(chords(&[p0, c[0], c[1], c[2]])); + } + points + } +} + +fn eval(c: &[Point; 4], t: f64) -> Point { + let u = 1.0 - t; + let (a, b, d, e) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t); + (a * c[0].0 + b * c[1].0 + d * c[2].0 + e * c[3].0, a * c[0].1 + b * c[1].1 + d * c[2].1 + e * c[3].1) +} + +fn derivative(c: &[Point; 4], t: f64) -> Point { + let u = 1.0 - t; + let (a, b, d) = (3.0 * u * u, 6.0 * u * t, 3.0 * t * t); + ( + a * (c[1].0 - c[0].0) + b * (c[2].0 - c[1].0) + d * (c[3].0 - c[2].0), + a * (c[1].1 - c[0].1) + b * (c[2].1 - c[1].1) + d * (c[3].1 - c[2].1), + ) +} + +/// A device coordinate in tiny-skia's fixed point: 1/64ths of a sample. +fn dot6(v: f64) -> i64 { + (v * (GRID * 64) as f64) as i64 +} + +/// A stroke's path, as many chords as tiny-skia's outline of it has: the +/// offset `r` out from the cubic in quads, each halved until its middle is +/// within a quarter of a pixel of the offset's, then each quad in as many +/// chords as tiny-skia steps it in. +fn stroked(c: &[Point; 4], r: f64) -> Vec { + let tangent = |t: f64| { + let (x, y) = derivative(c, t); + let length = x.hypot(y); + assert!(length > 0.0, "a stroked curve with no direction at {t}"); + (x / length, y / length) + }; + let offset = |t: f64| { + let (p, d) = (eval(c, t), tangent(t)); + (p.0 - d.1 * r, p.1 + d.0 * r) + }; + let mut out = vec![]; + let mut pieces = vec![(0.0, 1.0)]; + while let Some((t0, t1)) = pieces.pop() { + let (o0, o1, d0, d1) = (offset(t0), offset(t1), tangent(t0), tangent(t1)); + let cross = d0.0 * d1.1 - d0.1 * d1.0; + if cross.abs() < 1e-9 { + // Straight: one chord. + out.push((t1, 1)); + continue; + } + // Where the offset's end tangents meet: the quad's control point. + let s = ((o1.0 - o0.0) * d1.1 - (o1.1 - o0.1) * d1.0) / cross; + let q = (o0.0 + d0.0 * s, o0.1 + d0.1 * s); + let mid = ((o0.0 + 2.0 * q.0 + o1.0) / 4.0, (o0.1 + 2.0 * q.1 + o1.1) / 4.0); + let half = f64::midpoint(t0, t1); + let want = offset(half); + if (mid.0 - want.0).hypot(mid.1 - want.1) > 0.25 { + pieces.push((half, t1)); + pieces.push((t0, half)); + continue; + } + let bend = |a: f64, b: f64, c: f64| (2 * dot6(b) - dot6(a) - dot6(c)) >> 2; + out.push((t1, chords(bend(o0.0, q.0, o1.0), bend(o0.1, q.1, o1.1), 0))); + } + let mut points = vec![]; + let mut t0 = 0.0; + for (t1, n) in out { + points.extend((1..=n).map(|i| eval(c, t0 + (t1 - t0) * i as f64 / n as f64))); + t0 = t1; + } + points +} + +/// How many chords tiny-skia steps a curve in, from how far it strays from +/// its chord in each axis, in 1/64ths of a sample: `2^(shift + extra)`, at +/// least two and at most 64. +fn chords(dx: i64, dy: i64, extra: i64) -> usize { + let (dx, dy) = (dx.abs(), dy.abs()); + let distance = (dx.max(dy) + (dx.min(dy) >> 1) + 16) >> 5; + let shift = ((64 - distance.leading_zeros() as i64) >> 1) + extra; + 1 << shift.clamp(1, 6) +} + +/// A fill's edges as tiny-skia steps them: the cubic cut where it turns in y, +/// and each piece in `2^shift` chords, `shift` from how far its control points +/// stray from its chord in 1/64ths of a sample. +fn stepped(c: &[Point; 4]) -> Vec { + let (a, b, d) = (c[1].1 - c[0].1, c[2].1 - c[1].1, c[3].1 - c[2].1); + // Where dy/dt, `(a - 2b + d)t^2 + 2(b - a)t + a`, is zero. + let (qa, qb) = (a - 2.0 * b + d, 2.0 * (b - a)); + let mut turns: Vec = if qa.abs() < 1e-12 { + if qb != 0.0 { vec![-a / qb] } else { vec![] } + } else { + let disc = qb * qb - 4.0 * qa * a; + if disc < 0.0 { vec![] } else { [-1.0, 1.0].map(|s| (-qb + s * disc.sqrt()) / (2.0 * qa)).to_vec() } + }; + turns.retain(|t| *t > 0.0 && *t < 1.0); + turns.sort_by(f64::total_cmp); + turns.dedup(); + let mut bounds = vec![0.0]; + bounds.extend(turns); + bounds.push(1.0); + + let mut out = vec![]; + for piece in bounds.windows(2) { + let (t0, t1) = (piece[0], piece[1]); + let at = |s: f64| eval(c, t0 + (t1 - t0) * s); + // The piece's own control points, by its end tangents. + let span = t1 - t0; + let (p0, p3) = (at(0.0), at(1.0)); + let (d0, d3) = (derivative(c, t0), derivative(c, t1)); + let p1 = (p0.0 + d0.0 * span / 3.0, p0.1 + d0.1 * span / 3.0); + let p2 = (p3.0 - d3.0 * span / 3.0, p3.1 - d3.1 * span / 3.0); + // How far the curve at 1/3 and 2/3 strays from the control points + // there, with 19/512 for 1/27. + let delta = |a: f64, b: f64, c: f64, d: f64| { + let (a, b, c, d) = (dot6(a), dot6(b), dot6(c), dot6(d)); + let third = ((a * 8 - b * 15 + 6 * c + d) * 19) >> 9; + let two_thirds = ((a + 6 * b - c * 15 + d * 8) * 19) >> 9; + third.abs().max(two_thirds.abs()) + }; + let n = chords(delta(p0.0, p1.0, p2.0, p3.0), delta(p0.1, p1.1, p2.1, p3.1), 1); + out.extend((1..=n).map(|i| at(i as f64 / n as f64))); + } + out +} + +/// Each pixel's coverage, `size` by `size`, row by row. +fn coverage(icon: &Icon, size: usize) -> Vec { + let scale = size as f64 / icon.edge; + let device = |(x, y): Point| ((x - icon.origin.0) * scale, (y - icon.origin.1) * scale); + + let mut out = vec![0u8; size * size]; + let mut spans: Vec<(f64, f64)> = vec![]; + let mut crossings: Vec<(f64, i32)> = vec![]; + for shape in &icon.shapes { + let subpaths: Vec = shape.subpaths.iter().map(|s| s.map(device)).collect(); + let fills: Vec> = if shape.fill { subpaths.iter().map(|s| s.flatten(stepped)).collect() } else { vec![] }; + let r = shape.stroke.unwrap_or(0.0) * scale; + let strokes: Vec<(Vec, bool)> = match shape.stroke { + Some(_) => subpaths.iter().map(|s| (s.flatten(|c| stroked(c, r)), s.closed)).collect(), + None => vec![], + }; + + let mut alpha = vec![0u32; size * size]; + for row in 0..size * GRID { + let y = (row as f64 + 0.5) / GRID as f64; + spans.clear(); + crossings.clear(); + for points in &fills { + for (a, b) in edges(points, true) { + if (a.1 < y && y <= b.1) || (b.1 < y && y <= a.1) { + let x = a.0 + (y - a.1) * (b.0 - a.0) / (b.1 - a.1); + crossings.push((x, if b.1 > a.1 { 1 } else { -1 })); + } + } + } + crossings.sort_by(|a, b| a.0.total_cmp(&b.0)); + let mut winding = 0; + for &(x, dir) in &crossings { + if winding == 0 { + spans.push((x, f64::INFINITY)); + } + winding += dir; + if winding == 0 { + spans.last_mut().unwrap().1 = x; + } + } + for (points, closed) in &strokes { + for (a, b) in edges(points, *closed) { + spans.extend(capsule(a, b, r, y)); + } + } + spans.sort_by(|a, b| a.0.total_cmp(&b.0)); + let mut counts = vec![0u32; size]; + let mut i = 0; + while i < spans.len() { + let (a, mut b) = spans[i]; + i += 1; + while i < spans.len() && spans[i].0 <= b { + b = b.max(spans[i].1); + i += 1; + } + // The sample points `(column + 0.5) / GRID` in `a < x <= b`. + let first = ((a * GRID as f64 - 0.5).floor() + 1.0).max(0.0); + let last = (b * GRID as f64 - 0.5).floor().min((size * GRID) as f64 - 1.0); + if first <= last { + for column in first as usize..=last as usize { + counts[column / GRID] += 1; + } + } + } + // A row of four is worth 64, but 63 in a pixel's last row, so that + // a covered pixel sums to 255 and not 256. + let full = if row % GRID == GRID - 1 { 63 } else { 64 }; + let pixels = &mut alpha[(row / GRID) * size..][..size]; + for (pixel, &n) in pixels.iter_mut().zip(&counts) { + *pixel += if n == GRID as u32 { full } else { n * 16 }; + } + } + for (o, &a) in out.iter_mut().zip(&alpha) { + *o = ((255 * a + *o as u32 * (255 - a) + 255) >> 8) as u8; + } + } + out +} + +/// A polyline's segments, with the closing one where `closed`. +fn edges(points: &[Point], closed: bool) -> impl Iterator + '_ { + let closing = (closed && points.len() > 1).then(|| (points[points.len() - 1], points[0])); + points.windows(2).map(|w| (w[0], w[1])).chain(closing) +} + +/// Where the row `y` crosses the points within `r` of the segment `a`-`b`. +fn capsule(a: Point, b: Point, r: f64, y: f64) -> Option<(f64, f64)> { + let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY); + for c in [a, b] { + let dy = y - c.1; + if dy.abs() <= r { + let h = (r * r - dy * dy).sqrt(); + lo = lo.min(c.0 - h); + hi = hi.max(c.0 + h); + } + } + let (dx, dy) = (b.0 - a.0, b.1 - a.1); + let length = dx.hypot(dy); + if length > 0.0 { + // Along the segment, 0..=1, and across it, -r..=r, are each linear in x. + let along = solve(dx / (length * length), (y - a.1) * dy / (length * length), 0.0, 1.0); + let across = solve(dy / length, -(y - a.1) * dx / length, -r, r); + if let (Some(p), Some(q)) = (along, across) { + let (from, to) = (p.0.max(q.0), p.1.min(q.1)); + if from <= to { + lo = lo.min(from + a.0); + hi = hi.max(to + a.0); + } + } + } + (lo <= hi).then_some((lo, hi)) +} + +/// The `x` at which `slope * x + offset` lies in `low..=high`, relative to +/// the segment's start. +fn solve(slope: f64, offset: f64, low: f64, high: f64) -> Option<(f64, f64)> { + if slope == 0.0 { + return (low..=high).contains(&offset).then_some((f64::NEG_INFINITY, f64::INFINITY)); + } + let (p, q) = ((low - offset) / slope, (high - offset) / slope); + Some((p.min(q), p.max(q))) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn draw(body: &str, edge: u32, size: usize) -> Vec { + let svg = format!( + "{body}" + ); + coverage(&Icon::parse(&svg), size) + } + + /// A covered pixel is 255, and one a quarter covered is four samples of 16: + /// a square on pixel boundaries, and one half a pixel off them. + #[test] + fn a_square_covers_its_samples() { + let on = draw("", 4, 4); + assert_eq!(on, [0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 0]); + let off = draw("", 4, 4); + assert_eq!(off, [64, 64, 0, 0, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + } + + /// Against the area of a disc and of a ring, the oracle for the arc + /// conversion and the stroke that needs no resvg: a filled circle drawn as + /// two arcs, and a circle's round stroke, each within 0.5% of its area. + #[test] + fn a_disc_and_a_ring_cover_their_areas() { + let area = |cov: &[u8]| cov.iter().map(|&a| a as f64 / 255.0).sum::(); + let pi = std::f64::consts::PI; + let disc = draw("", 256, 256); + let want = pi * 100.0 * 100.0; + assert!((area(&disc) - want).abs() < want * 5e-3, "disc: {} of {want}", area(&disc)); + let ring = draw( + "", + 256, + 256, + ); + let want = pi * (108.0f64.powi(2) - 84.0f64.powi(2)); + assert!((area(&ring) - want).abs() < want * 5e-3, "ring: {} of {want}", area(&ring)); + } + + /// The committed icons draw to the pixels the owner accepted against + /// resvg's, which no reading of this file can see. + #[test] + fn the_icons_are_the_pixels_compared_with_resvg() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let mut digest = toyos_sha2_hw::sha256(); + for (stem, _) in SIZES { + let svg = std::fs::read(root.join(format!("assets/icons/{stem}.svg"))).expect("a committed icon"); + digest.update(rasterize(stem, &svg)); + } + let hex: String = digest.finalize().iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!( + hex, + "bc246dcfb81736e4aef449383ea3c666e38122ca47d0b2a2dfead9302653929e", + "the icons' pixels moved: draw them against resvg 0.47 out of tree with the differential \ + posted on pull request #814, and ask the owner again on any visible change before \ + pinning the new digest" + ); + } + + /// A contour wound against its outer one is a hole; one wound with it is not. + #[test] + fn a_fill_is_nonzero() { + let hole = draw("", 3, 3); + assert_eq!(hole, [255, 255, 255, 255, 0, 255, 255, 255, 255]); + let solid = draw("", 3, 3); + assert_eq!(solid, [255; 9]); + } + + #[test] + #[should_panic(expected = "paint \"#ff0000\" is neither currentColor nor none")] + fn a_literal_colour_is_refused() { + draw("", 4, 4); + } + + #[test] + #[should_panic(expected = " fills black")] + fn an_unpainted_shape_is_refused() { + draw("", 4, 4); + } + + #[test] + #[should_panic(expected = " is stroked without round caps and joins")] + fn a_square_cap_is_refused() { + draw("", 4, 4); + } + + #[test] + #[should_panic(expected = " is not read")] + fn an_unread_attribute_is_refused() { + draw("", 4, 4); + } + + #[test] + #[should_panic(expected = " has children")] + fn a_group_is_refused() { + draw("", 4, 4); + } + + #[test] + #[should_panic(expected = "path command 'Q' is not read")] + fn an_unread_command_is_refused() { + draw("", 4, 4); + } +} diff --git a/src/image.rs b/src/image.rs index 2f4407abd53..5ccd1e378fe 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::num::NonZeroU64; use std::path::Path; @@ -7,6 +6,7 @@ use bcachefs::{BlockBuf, BlockIO, BlockNum, Formatted, FsUuid, Superblock, VecBl use crate::arch::Arch; use toyos_fat32::{BlockAccess, Fat32, FatTime, IoError}; +use toyos_gpt::Guid; /// The image that goes on the ROOT partition, named by a UUID **derived, never /// drawn**: two builds of one tree have to agree on the name the kernel @@ -236,18 +236,16 @@ pub fn create_boot_image( // kernel. The kernel is given the partition by name; nothing anywhere goes // looking for one by type or by format. Every slot partition is named the // same way, by the slot table. - let guid = uuid::Uuid::new_v4; - let (log_guid, esp_guid, table_guid) = (guid(), guid(), guid()); - let a = (guid(), guid()); - let b = second.map(|room| (guid(), guid(), room)); - let raw = |g: uuid::Uuid| g.to_bytes_le(); + let (log_guid, esp_guid, table_guid) = (crate::gptwrite::random_guid(), crate::gptwrite::random_guid(), crate::gptwrite::random_guid()); + let a = (crate::gptwrite::random_guid(), crate::gptwrite::random_guid()); + let b = second.map(|room| (crate::gptwrite::random_guid(), crate::gptwrite::random_guid(), room)); let s = sections(kernel_bytes, root_bytes, params, signing); // Room for a kernel twice this one's size beside it, so an update can // write a new kernel into a slot's volume before the old one is gone. let slot_bytes = round_up_sectors(((kernel_bytes.len() * 2 + ESP_FREE_BYTES) * 64 / 63).max(FAT32_MIN_BYTES)); let a_boot = create_slot_volume(Some(&s), slot_bytes); - let slot = |(boot, root): (uuid::Uuid, uuid::Uuid), version| toyos_update::slots::Slot { boot: raw(boot), root: raw(root), version }; + let slot = |(boot, root): (Guid, Guid), version| toyos_update::slots::Slot { boot: boot.0, root: root.0, version }; let table = toyos_update::slots::Table { sequence: 1, marked: toyos_update::slots::Which::A, @@ -257,21 +255,21 @@ pub fn create_boot_image( table_volume[..toyos_update::slots::BLOCK].copy_from_slice(&table.encode()); let mut parts = vec![ - Part::full("ESP", "EFI System", gpt::partition_types::EFI, esp_guid, create_esp_volume(arch, bl_bytes, log_guid), Some(Volume::Fat32)), - Part::full("slot table", "ToyOS slots", TOYOS_SLOTS, table_guid, table_volume, None), + Part::full("ESP", "EFI System", Guid::EFI_SYSTEM, esp_guid, create_esp_volume(arch, bl_bytes, log_guid), Some(Volume::Fat32)), + Part::full("slot table", "ToyOS slots", Guid::TOYOS_SLOTS, table_guid, table_volume, None), // Microsoft Basic Data, and that type is the whole reason this is a // partition of its own: macOS never auto-mounts an EFI-typed partition // and this host refuses even a manual non-root mount of one, so a log on // the ESP is unreachable without the admin account. This type mounts in // Finder, in Windows and in Linux on plug-in, with nothing configured. - Part::full("log partition", "ToyOS log", gpt::partition_types::BASIC, log_guid, create_log_volume(), Some(Volume::Fat32)), - Part::full("slot A's volume", "ToyOS slot A", TOYOS_BOOT, a.0, a_boot, Some(Volume::Fat32)), - Part::full("slot A's ROOT", "ToyOS root A", TOYOS_ROOT, a.1, root_bytes.to_vec(), Some(Volume::Root)), + Part::full("log partition", "ToyOS log", Guid::MICROSOFT_BASIC, log_guid, create_log_volume(), Some(Volume::Fat32)), + Part::full("slot A's volume", "ToyOS slot A", Guid::TOYOS_BOOT, a.0, a_boot, Some(Volume::Fat32)), + Part::full("slot A's ROOT", "ToyOS root A", Guid::TOYOS_ROOT, a.1, root_bytes.to_vec(), Some(Volume::Root)), ]; if let Some((boot, root, room)) = b { - parts.push(Part::full("slot B's volume", "ToyOS slot B", TOYOS_BOOT, boot, create_slot_volume(None, slot_bytes), Some(Volume::Fat32))); + parts.push(Part::full("slot B's volume", "ToyOS slot B", Guid::TOYOS_BOOT, boot, create_slot_volume(None, slot_bytes), Some(Volume::Fat32))); let len = align_up(room.root_bytes as usize, PARTITION_ALIGN) as u64; - parts.push(Part { what: "slot B's ROOT", name: "ToyOS root B", kind: TOYOS_ROOT, guid: root, bytes: Vec::new(), len, judge: None }); + parts.push(Part { what: "slot B's ROOT", name: "ToyOS root B", kind: Guid::TOYOS_ROOT, guid: root, bytes: Vec::new(), len, judge: None }); } create_gpt_disk(&parts) } @@ -455,8 +453,8 @@ const PARTITION_ALIGN: usize = 1024 * 1024; /// The smallest volume there is a FAT32 for. /// -/// FAT32 *is* the format with at least 65,525 clusters, and `fatfs` gives a -/// volume this size 512-byte clusters — so the data area alone is 33.5 MiB +/// FAT32 *is* the format with at least 65,525 clusters, and [`crate::fatformat`] +/// gives a volume this size 512-byte clusters — so the data area alone is 33.5 MiB /// before the two FATs and the reserved sectors. Measured at exactly this size: /// the format succeeds and `fsck_msdos` reports 68,551 free clusters. const FAT32_MIN_BYTES: usize = 34 * 1024 * 1024; @@ -465,7 +463,7 @@ const FAT32_MIN_BYTES: usize = 34 * 1024 * 1024; /// /// The `64/63` beside this at the one call site is headroom for the two FAT /// copies, which cost a byte of table per 64 bytes of volume at the 512-byte -/// clusters `fatfs` gives a small one — the worst case, so a volume whose +/// clusters [`crate::fatformat`] gives a small one — the worst case, so a volume whose /// clusters are larger is over-provisioned rather than under. A flat slack /// leaves a guest whatever the rounding did, and an fsync that fails while the /// host-side volume reports megabytes free is the symptom of getting it wrong. @@ -478,7 +476,7 @@ fn align_up(n: usize, to: usize) -> usize { /// A FAT volume label: eleven bytes of space-padded OEM text. /// /// Without one every host calls the volume `NO NAME`, which is what the ESP -/// showed as. `format_volume` writes it into both places the format keeps it, +/// showed as. The format writes it into both places the format keeps it, /// the BPB field and a `VOLUME_ID` entry in the root directory, and the mount /// on macOS is named from it — measured, `/Volumes/TOYOS-LOG`. fn fat_label(text: &str) -> [u8; 11] { @@ -494,15 +492,7 @@ fn fat_label(text: &str) -> [u8; 11] { /// An empty FAT32 volume of `bytes`, under `label`. fn format_fat32(bytes: usize, label: &str) -> Vec { - let mut volume = vec![0u8; bytes]; - fatfs::format_volume( - Cursor::new(&mut volume), - fatfs::FormatVolumeOptions::new() - .fat_type(fatfs::FatType::Fat32) - .volume_label(fat_label(label)), - ) - .unwrap_or_else(|e| panic!("failed to format the {label} volume: {e}")); - volume + crate::fatformat::format(bytes, fat_label(label)) } /// The volume being built, as [`toyos_fat32`] sees it. @@ -511,7 +501,7 @@ fn format_fat32(bytes: usize, label: &str) -> Vec { /// volume is a `Vec` in this process. The kernel's adapter is where the /// read-modify-write against 4 KiB device blocks lives, and `toyos-fat32`'s own /// host suite is where that shape is exercised. -struct VolumeIo<'a>(&'a mut [u8]); +pub(crate) struct VolumeIo<'a>(pub(crate) &'a mut [u8]); impl VolumeIo<'_> { fn range(&self, offset: u64, len: usize) -> Result, IoError> { @@ -562,17 +552,11 @@ fn build_time() -> FatTime { /// Write `files` onto a formatted volume, creating the directories they name, /// and leave the free-cluster count recorded. /// -/// Written with **our** FAT32 driver rather than with the crate that formatted -/// the volume: `fatfs`'s `create_dir` writes a long-name entry ahead of each -/// `.` and `..`, which the format requires to be a subdirectory's first two -/// entries, and gives `..` the root's cluster number where the format requires -/// zero — neither reachable through that crate's API. `toyos-fat32` is also the -/// driver the kernel appends `kernel.log` with and the one its own host suite -/// runs the volume checker against, so "the image we build is clean" is a claim -/// about the writer that is judged. `fatfs` keeps the format call, where an -/// empty volume has no subdirectory for either bug to live in. +/// Written with `toyos-fat32`, the driver the kernel appends `kernel.log` with +/// and the one its own host suite runs the volume checker against, so "the image +/// we build is clean" is a claim about the writer that is judged. /// -/// `format_volume` leaves FSInfo's free-cluster field 0xFFFFFFFF, which FAT32 +/// The format leaves FSInfo's free-cluster field 0xFFFFFFFF, which FAT32 /// defines as "unknown" and every host reports free space from; `free_bytes` /// counts the FAT when the volume arrived without a hint and `sync` writes it. fn populate(volume: &mut [u8], label: &str, files: &[(&str, &[u8])]) { @@ -601,7 +585,7 @@ fn populate(volume: &mut [u8], label: &str, files: &[(&str, &[u8])]) { /// partition the kernel's log goes on. The kernel and its parameter are a /// slot's (`create_slot_volume`), because the loader is the one part of the /// machine that is not slotted. -fn create_esp_volume(arch: Arch, bootloader: &[u8], log_guid: uuid::Uuid) -> Vec { +fn create_esp_volume(arch: Arch, bootloader: &[u8], log_guid: Guid) -> Vec { let total_size = round_up_sectors(((bootloader.len() + ESP_FREE_BYTES) * 64 / 63).max(FAT32_MIN_BYTES)); let mut volume = format_fat32(total_size, "TOYOS-BOOT"); populate( @@ -615,7 +599,7 @@ fn create_esp_volume(arch: Arch, bootloader: &[u8], log_guid: uuid::Uuid) -> Vec // order: nothing converts them on the way to the kernel and nothing // converts the table's, so the comparison that decides which // partition holds the log cannot be got backwards. - ("toyos/log.guid", &log_guid.to_bytes_le()), + ("toyos/log.guid", &log_guid.0), ], ); volume @@ -656,34 +640,6 @@ fn create_log_volume() -> Vec { volume } -/// `B350BC93-…`, the TOYOS-ROOT partition type, as the writer names a type. -/// -/// `toyos_gpt::Guid::TOYOS_ROOT` is the same sixteen bytes as the kernel -/// matches them, and `toyos_root_text_is_the_type_guid` is what keeps the two -/// spellings one constant — and the same for each type below. -const TOYOS_ROOT: gpt::partition_types::Type = gpt::partition_types::Type { - guid: toyos_gpt::Guid::TOYOS_ROOT_TEXT, - os: gpt::partition_types::OperatingSystem::None, -}; - -/// `064E3777-…`, the TOYOS-DATA partition type, the same way round. -const TOYOS_DATA: gpt::partition_types::Type = gpt::partition_types::Type { - guid: toyos_gpt::Guid::TOYOS_DATA_TEXT, - os: gpt::partition_types::OperatingSystem::None, -}; - -/// `94464329-…`, the slot table's partition type. -const TOYOS_SLOTS: gpt::partition_types::Type = gpt::partition_types::Type { - guid: toyos_gpt::Guid::TOYOS_SLOTS_TEXT, - os: gpt::partition_types::OperatingSystem::None, -}; - -/// `037719D7-…`, a slot's FAT volume. -const TOYOS_BOOT: gpt::partition_types::Type = gpt::partition_types::Type { - guid: toyos_gpt::Guid::TOYOS_BOOT_TEXT, - os: gpt::partition_types::OperatingSystem::None, -}; - /// Lay a table on the disk at `path`, already `len` bytes long, carrying one /// TOYOS-DATA partition, stamp the designation at its first block, and answer /// where it landed. The table and the stamp are all this writes, so a sparse @@ -697,26 +653,26 @@ pub fn designate_data_disk(path: &Path, len: u64) -> (u64, u64) { else { panic!("a {len}-byte disk has no room for a DATA partition"); }; - let file = std::fs::OpenOptions::new() + let mut file = std::fs::OpenOptions::new() .read(true) .write(true) .open(path) .unwrap_or_else(|e| panic!("open {} to partition it: {e}", path.display())); - let (mut device, starts) = lay(Box::new(file), len, &[data_row(uuid::Uuid::new_v4(), data_bytes)]); - designate(&mut device, starts[0], data_bytes); - device.flush().expect("flush the data disk"); + let starts = lay(&mut file, len, &[data_row(crate::gptwrite::random_guid(), data_bytes)]); + write_at(&mut file, starts[0], &designation(data_bytes / SECTOR as u64), "DATA partition's designation"); (starts[0], data_bytes) } /// A TOYOS-DATA partition's entry. -fn data_row(guid: uuid::Uuid, len: u64) -> Row { - Row { what: "DATA partition".into(), name: "ToyOS data".into(), kind: TOYOS_DATA, guid, len } +fn data_row(guid: Guid, len: u64) -> Row { + Row { what: "DATA partition".into(), name: "ToyOS data".into(), kind: Guid::TOYOS_DATA, guid, len } } -/// Stamp the designation on the `bytes`-long partition at byte `at` of `device`. -fn designate(device: &mut gpt::DiskDeviceObject<'_>, at: u64, bytes: u64) { - device.seek(SeekFrom::Start(at)).expect("seek to the data partition"); - device.write_all(&designation(bytes / SECTOR as u64)).expect("stamp the data partition"); +/// Write `bytes` at byte `at` of `disk`; `what` they are is what a refusal names. +fn write_at(disk: &mut (impl Write + Seek), at: u64, bytes: &[u8], what: &str) { + disk.seek(SeekFrom::Start(at)) + .and_then(|_| disk.write_all(bytes)) + .unwrap_or_else(|e| panic!("write the {what} at byte {at}: {e}")); } /// What a machine's own disk holds once the boot image `image` is installed @@ -729,29 +685,21 @@ fn designate(device: &mut gpt::DiskDeviceObject<'_>, at: u64, bytes: u64) { /// blocks that are not zero are written, so the file costs the host what the /// image does. pub fn install(image: &[u8], disk: &Path, data_bytes: u64) { - use gpt::disk::LogicalBlockSize::Lb512; let mut listed = [None; 16]; let scan = toyos_gpt::list(&mut ImageSectors(image), &mut listed) .unwrap_or_else(|e| panic!("the image to install carries no partition table: {e:?}")); assert!(scan.matched as usize <= listed.len(), "the image to install carries {} partitions", scan.matched); - // The names, which the reader above does not read; the types, which this one does not keep. - let mut names: BTreeMap = gpt::header::read_header_from_arbitrary_device(&mut Cursor::new(image), Lb512) - .and_then(|header| gpt::partition::file_read_partitions(&mut Cursor::new(image), &header, Lb512)) - .unwrap_or_else(|e| panic!("the image to install carries no readable partition entries: {e}")) - .into_values() - .map(|part| (part.part_guid, part.name)) - .collect(); let carried: Vec<(Row, &[u8])> = listed .iter() .flatten() .map(|entry| { let part = entry.unwrap_or_else(|unplaced| panic!("the image to install states no partition: {unplaced:?}")); - let guid = uuid::Uuid::from_bytes_le(part.unique_guid().0); - let kind = [gpt::partition_types::EFI, gpt::partition_types::BASIC, TOYOS_SLOTS, TOYOS_BOOT, TOYOS_ROOT] - .into_iter() - .find(|kind| uuid::Uuid::parse_str(kind.guid).is_ok_and(|text| text.to_bytes_le() == part.type_guid().0)) - .unwrap_or_else(|| panic!("partition {guid} is of type {}, which no boot image carries", part.type_guid())); - let name = names.remove(&guid).unwrap_or_else(|| panic!("partition {guid} has no name")); + let (guid, kind) = (part.unique_guid(), part.type_guid()); + assert!( + [Guid::EFI_SYSTEM, Guid::MICROSOFT_BASIC, Guid::TOYOS_SLOTS, Guid::TOYOS_BOOT, Guid::TOYOS_ROOT].contains(&kind), + "partition {guid} is of type {kind}, which no boot image carries" + ); + let name = name_of(&part); let at = part.first_lba() as usize * LBA as usize; let bytes = &image[at..at + part.lba_count().get() as usize * LBA as usize]; (Row { what: format!("installed {name}"), name, kind, guid, len: bytes.len() as u64 }, bytes) @@ -759,10 +707,10 @@ pub fn install(image: &[u8], disk: &Path, data_bytes: u64) { .collect(); let (mut rows, volumes): (Vec, Vec<&[u8]>) = carried.into_iter().unzip(); - rows.insert(0, data_row(uuid::Uuid::new_v4(), data_bytes)); + rows.insert(0, data_row(crate::gptwrite::random_guid(), data_bytes)); let total = disk_bytes(rows.iter().map(|row| row.len)); - let file = std::fs::OpenOptions::new() + let mut file = std::fs::OpenOptions::new() .read(true) .write(true) .create_new(true) @@ -770,18 +718,18 @@ pub fn install(image: &[u8], disk: &Path, data_bytes: u64) { .unwrap_or_else(|e| panic!("{} is not installed on, being no file this install made: {e}", disk.display())); file.set_len(total).unwrap_or_else(|e| panic!("size {} to {total} bytes: {e}", disk.display())); - let (mut device, starts) = lay(Box::new(file), total, &rows); - designate(&mut device, starts[0], data_bytes); + let starts = lay(&mut file, total, &rows); + write_at(&mut file, starts[0], &designation(data_bytes / SECTOR as u64), "DATA partition's designation"); for ((row, volume), start) in rows[1..].iter().zip(volumes).zip(&starts[1..]) { for (i, block) in volume.chunks(PARTITION_ALIGN).enumerate().filter(|(_, block)| block.iter().any(|byte| *byte != 0)) { - let at = start + (i * PARTITION_ALIGN) as u64; - device - .seek(SeekFrom::Start(at)) - .and_then(|_| device.write_all(block)) - .unwrap_or_else(|e| panic!("write the {} at byte {at}: {e}", row.what)); + write_at(&mut file, start + (i * PARTITION_ALIGN) as u64, block, &row.what); } } - device.flush().unwrap_or_else(|e| panic!("flush {}: {e}", disk.display())); +} + +/// The name a table this build wrote gives `part`. +fn name_of(part: &toyos_gpt::Partition) -> String { + String::from_utf16(part.name()).unwrap_or_else(|e| panic!("partition {}'s name is no UTF-16: {e}", part.unique_guid())) } /// Block 0 of a volume: the magic and its block count. @@ -820,8 +768,8 @@ impl toyos_gpt::Sectors for FileSectors<'_> { struct Part { what: &'static str, name: &'static str, - kind: gpt::partition_types::Type, - guid: uuid::Uuid, + kind: Guid, + guid: Guid, bytes: Vec, len: u64, judge: Option, @@ -829,14 +777,7 @@ struct Part { impl Part { /// A partition exactly as long as its bytes. - fn full( - what: &'static str, - name: &'static str, - kind: gpt::partition_types::Type, - guid: uuid::Uuid, - bytes: Vec, - judge: Option, - ) -> Self { + fn full(what: &'static str, name: &'static str, kind: Guid, guid: Guid, bytes: Vec, judge: Option) -> Self { let len = bytes.len() as u64; Self { what, name, kind, guid, bytes, len, judge } } @@ -847,8 +788,8 @@ impl Part { struct Row { what: String, name: String, - kind: gpt::partition_types::Type, - guid: uuid::Uuid, + kind: Guid, + guid: Guid, len: u64, } @@ -860,87 +801,40 @@ fn disk_bytes(lens: impl Iterator) -> u64 { round_up_sectors(align_up(end, PARTITION_ALIGN) + PARTITION_ALIGN) as u64 } -/// Write a protective MBR and a GPT on `device`, `total` bytes long, placing -/// `rows` in their order, each on a [`PARTITION_ALIGN`] boundary and under -/// its own GUID: the device, and the byte each row starts at. -fn lay<'a>( - mut device: gpt::DiskDeviceObject<'a>, - total: u64, - rows: &[Row], -) -> (gpt::DiskDeviceObject<'a>, Vec) { - assert_eq!(total % 512, 0, "image must be a whole number of 512-byte sectors to be flashable"); - let mbr = gpt::mbr::ProtectiveMBR::with_lb_size(u32::try_from(total / 512 - 1).unwrap_or(0xFF_FF_FF_FF)); - mbr.overwrite_lba0(&mut device).expect("failed to write MBR"); - - let mut gdisk = gpt::GptConfig::default() - .initialized(false) - .writable(true) - .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) - .create_from_device(device, None) - .expect("failed to create GPT disk"); - - gdisk - .update_partitions(BTreeMap::::new()) - .expect("failed to initialize partition table"); - - let align = Some((PARTITION_ALIGN / 512) as u64); - let ids: Vec = rows +/// Write a protective MBR and a GPT on `disk`, `total` bytes long, placing +/// `rows` in their order, each on the first [`PARTITION_ALIGN`] boundary after +/// the last and under its own GUID: the byte each row starts at. +fn lay(disk: &mut (impl Write + Seek), total: u64, rows: &[Row]) -> Vec { + assert_eq!(total % u64::from(LBA), 0, "image must be a whole number of {LBA}-byte sectors to be flashable"); + let mut end = 0; + let starts: Vec = rows .iter() .map(|row| { - gdisk - .add_partition(&row.name, row.len, row.kind.clone(), 0, align) - .unwrap_or_else(|e| panic!("failed to add the {}: {e}", row.what)) + // The invariant [`PARTITION_ALIGN`] exists for: the kernel mounts + // several at once over one 4 KiB block device, and a device block + // belonging to two volumes would be cached twice. + assert_eq!(row.len % SECTOR as u64, 0, "the {} is {} bytes, not whole {SECTOR}-byte blocks", row.what, row.len); + let start = align_up(end.max(1), PARTITION_ALIGN) as u64; + end = (start + row.len) as usize; + start }) .collect(); - - // The GUID `add_partition` drew for each is discarded for the one its row - // names: the log's is written on the ESP, every slot partition's in the - // slot table, and the ESP's own because a firmware boot entry names a - // partition by GUID and every name is drawn once. - let mut table = gdisk.partitions().clone(); - for (row, id) in rows.iter().zip(&ids) { - table.get_mut(id).expect("a partition that was just added").part_guid = row.guid; - } - gdisk.update_partitions(table).expect("failed to stamp the partitions' unique GUIDs"); - - let placed: Vec<(&str, u64, u64)> = rows + let lba = u64::from(LBA); + let entries: Vec> = rows .iter() - .zip(&ids) - .map(|(row, id)| { - let entry = gdisk.partitions().get(id).expect("a partition that was just added"); - let start = entry.bytes_start(gpt::disk::LogicalBlockSize::Lb512).expect("a partition's start"); - let len = entry.bytes_len(gpt::disk::LogicalBlockSize::Lb512).expect("a partition's length"); - // A slot's ROOT is read for exactly its signed length, and a - // partition the table made any other length than asked is one - // whose contents nothing here placed. - assert_eq!(len, row.len, "the table gives the {} {len} bytes for a {}-byte partition", row.what, row.len); - (row.what.as_str(), start, row.len) + .zip(&starts) + .map(|(row, start)| crate::gptwrite::Entry { + kind: row.kind, + unique: row.guid, + first_lba: start / lba, + last_lba: (start + row.len) / lba - 1, + name: &row.name, }) .collect(); - - // The invariant [`PARTITION_ALIGN`] exists for, checked rather than - // assumed: the kernel mounts several at once over one 4 KiB block device, - // and a device block belonging to two volumes would be cached twice. - let sector = SECTOR as u64; - for (what, start, len) in &placed { - assert_eq!(start % sector, 0, "the {what} starts at byte {start}, off a {SECTOR}-byte block"); - assert_eq!(len % sector, 0, "the {what} is {len} bytes, not whole {SECTOR}-byte blocks"); - } - let mut by_start = placed.clone(); - by_start.sort_by_key(|(_, start, _)| *start); - for (before, after) in by_start.iter().zip(&by_start[1..]) { - assert!( - before.1 + before.2 <= after.1, - "the {} runs to {} and the {} starts at {}", - before.0, - before.1 + before.2, - after.0, - after.1 - ); - } - - let device = gdisk.write().expect("failed to write GPT"); - (device, placed.iter().map(|(_, start, _)| *start).collect()) + let table = crate::gptwrite::table(total / lba, crate::gptwrite::random_guid(), &entries); + write_at(disk, 0, &table.primary, "partition table"); + write_at(disk, total - table.backup.len() as u64, &table.backup, "backup partition table"); + starts } fn create_gpt_disk(parts: &[Part]) -> Vec { @@ -949,32 +843,24 @@ fn create_gpt_disk(parts: &[Part]) -> Vec { } let rows: Vec = parts .iter() - .map(|part| Row { what: part.what.into(), name: part.name.into(), kind: part.kind.clone(), guid: part.guid, len: part.len }) + .map(|part| Row { what: part.what.into(), name: part.name.into(), kind: part.kind, guid: part.guid, len: part.len }) .collect(); let total_size = disk_bytes(rows.iter().map(|row| row.len)); let mut disk = vec![0u8; total_size as usize]; - let (mut disk_device, starts) = lay(Box::new(Cursor::new(&mut disk)), total_size, &rows); - - disk_device.seek(std::io::SeekFrom::Start(0)).expect("failed to seek"); - let mut final_bytes = vec![0u8; total_size as usize]; - disk_device.read_exact(&mut final_bytes).expect("failed to read disk"); - + let starts = lay(&mut Cursor::new(&mut disk), total_size, &rows); for (part, start) in parts.iter().zip(&starts) { let start = *start as usize; - final_bytes[start..start + part.bytes.len()].copy_from_slice(&part.bytes); + disk[start..start + part.bytes.len()].copy_from_slice(&part.bytes); } - let judged: Vec<(&str, toyos_gpt::Guid, Volume)> = parts - .iter() - .filter_map(|part| part.judge.map(|kind| (part.what, toyos_gpt::Guid(part.guid.to_bytes_le()), kind))) - .collect(); - certify(&final_bytes, &judged).unwrap_or_else(|refusal| panic!("{refusal}")); - - final_bytes + let judged: Vec<(&str, Guid, Volume)> = + parts.iter().filter_map(|part| part.judge.map(|kind| (part.what, part.guid, kind))).collect(); + certify(&disk, &judged).unwrap_or_else(|refusal| panic!("{refusal}")); + disk } /// A disk image as logical blocks, so a GPT parser can read it without a file. -struct ImageSectors<'a>(&'a [u8]); +pub(crate) struct ImageSectors<'a>(pub(crate) &'a [u8]); impl toyos_gpt::Sectors for ImageSectors<'_> { fn lba_bytes(&self) -> u32 { @@ -1112,7 +998,7 @@ mod tests { let key = key(); let s = sections(b"kernel", &tiny_root(), "", signing(&key)); for (what, volume) in [ - ("ESP", create_esp_volume(Arch::X86_64, b"bootloader", uuid::Uuid::new_v4())), + ("ESP", create_esp_volume(Arch::X86_64, b"bootloader", crate::gptwrite::random_guid())), ("slot volume", create_slot_volume(Some(&s), FAT32_MIN_BYTES)), ("empty slot volume", create_slot_volume(None, FAT32_MIN_BYTES)), ("log volume", create_log_volume()), @@ -1199,7 +1085,7 @@ mod tests { #[test] fn the_esp_and_a_slot_carry_what_the_bootloader_looks_for() { assert_eq!( - files_of(create_esp_volume(Arch::X86_64, b"bootloader", uuid::Uuid::new_v4())), + files_of(create_esp_volume(Arch::X86_64, b"bootloader", crate::gptwrite::random_guid())), ["EFI/BOOT/BOOTx64.EFI", "toyos/log.guid"] ); let key = key(); @@ -1256,9 +1142,11 @@ mod tests { assert_eq!(parts.signed, &signed); } - /// Every partition of `image` is on `disk` under its GUID with its bytes, - /// found by the reader the kernel and `diskserver` use. - fn carries_every_partition_of(disk: &mut std::fs::File, image: &[u8]) { + /// Every partition of `image` is on `disk` under its GUID, its name and with + /// its bytes, found by the reader the kernel and `diskserver` use: the + /// names, in the image's order. + fn carries_every_partition_of(disk: &mut std::fs::File, image: &[u8]) -> Vec { + let mut names = Vec::new(); let mut listed = [None; 16]; toyos_gpt::list(&mut ImageSectors(image), &mut listed).expect("the image's table"); let parts: Vec = listed.iter().flatten().map(|entry| entry.expect("a placed entry")).collect(); @@ -1274,7 +1162,10 @@ mod tests { .and_then(|_| disk.read_exact(&mut installed)) .expect("read it back"); assert!(installed == image[at..at + len], "{} was installed as other bytes", part.unique_guid()); + assert_eq!(name_of(&on_disk), name_of(&part), "{}", part.unique_guid()); + names.push(name_of(&part)); } + names } /// An install puts a designated DATA first and the image after it, on a @@ -1290,7 +1181,10 @@ mod tests { install(&image, &path, DATA); let mut disk = std::fs::File::open(&path).expect("open the disk"); - carries_every_partition_of(&mut disk, &image); + assert_eq!( + carries_every_partition_of(&mut disk, &image), + ["EFI System", "ToyOS slots", "ToyOS log", "ToyOS slot A", "ToyOS root A"] + ); let data = only_partition(&mut FileSectors(&mut disk), toyos_gpt::Guid::TOYOS_DATA).expect("one DATA partition"); assert_eq!( (data.first_lba() * u64::from(LBA), data.lba_count().get() * u64::from(LBA)), @@ -1362,9 +1256,9 @@ mod tests { /// **The independent oracle for ROOT**, asked of the finished image by /// readers that did not write it: `toyos-gpt` finds the partition by type - /// where the `gpt` crate placed it, `bcachefs`'s mount-and-read path lists + /// where the table placed it, `bcachefs`'s mount-and-read path lists /// what its format-and-write path put there, and `toyos-fat32` reads the - /// boot parameter off the ESP that `fatfs` formatted. + /// boot parameter off the ESP. /// /// Every name, size, content hash and symlink target against the list /// handed to [`create_root_image`] — a listing that merely parsed would diff --git a/src/lib.rs b/src/lib.rs index 3e8700e7da4..a34d08ce9f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,11 +12,15 @@ pub mod cicache; pub mod clang; pub mod clippy; pub mod compiler; +pub mod fatformat; pub mod firmware; #[cfg(test)] pub mod gitfixture; +pub mod gptwrite; +pub mod hostedclang; pub mod flags; pub mod hostws; +pub mod icons; pub mod identity; pub mod image; pub mod kernelconsole; diff --git a/src/libcxx.rs b/src/libcxx.rs index f714b9f94b8..35f57f0ecbc 100644 --- a/src/libcxx.rs +++ b/src/libcxx.rs @@ -40,9 +40,11 @@ pub(crate) const OPTIONS: [(&str, &str); 18] = [ ("LIBCXXABI_HAS_CXA_THREAD_ATEXIT_IMPL", "ON"), // The C library has no `dladdr`, so libunwind names no function it unwinds. ("LIBUNWIND_ADDITIONAL_COMPILE_FLAGS", "-D_LIBUNWIND_USE_DLADDR=0"), - // The C library has no directory, `stat` or path surface for it - // (`issues/libcxx-is-built-without-std-filesystem.md`). - ("LIBCXX_ENABLE_FILESYSTEM", "OFF"), + // `open` refuses every directory, so no descriptor names one for `openat` + // and `unlinkat` to resolve against: `remove_all` walks a directory + // iterator, as libc++'s Windows does + // (`issues/remove-all-follows-a-link-swapped-in-mid-walk.md`). + ("LIBCXX_ADDITIONAL_COMPILE_FLAGS", "-DREMOVE_ALL_USE_DIRECTORY_ITERATOR"), ("LIBCXX_INCLUDE_BENCHMARKS", "OFF"), ("LIBCXX_INCLUDE_TESTS", "OFF"), ]; diff --git a/src/licence.rs b/src/licence.rs index 619a7a37a83..793e8789cd5 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -507,12 +507,6 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "NOTICE", Terms::Spdx("LicenseRef-GeneralUser-GS-2.0"), ), - ( - "assets/wallpaper.jpg", - "b6f0c89bf966cfb458333b280614f0c7723615e42e340b9d43a760a64fe05976", - "ours: `cargo run -- --regen-wallpaper` writes it from src/wallpaper.rs", - Terms::Spdx("MIT OR Apache-2.0"), - ), ( "doom.jpg", "ae22f71dc732580bd4f789937c9fe564969029413fc2092f27bdae8d1ceaf8e3", diff --git a/src/llvm.rs b/src/llvm.rs index cb55f7a3998..7306e9e008c 100644 --- a/src/llvm.rs +++ b/src/llvm.rs @@ -5,12 +5,13 @@ //! the fork checkout's gitlink names (`sysroot::gitlink`, which refuses a //! checkout holding what no commit does and a gitlink staged and not committed), //! the committed tree of its `src/bootstrap` (one holding what no commit does is -//! refused), the bootstrap configuration below, [`RECIPE`], and the tools the +//! refused), the bootstrap configuration below, [`recipe`], and the tools the //! host builds it with ([`host_tools`]). `llvm//` in the store //! (`src/keystore.rs`) is what builds read of bootstrap's install of that LLVM and its clang //! ([`keep`]), with its LLD in `bin/` beside `llvm-config` and in `src/` the -//! runtimes' sources the C++ runtime is built from (`src/libcxx.rs`) as its -//! commit holds them, made by whichever build first needs it ([`resolve`]), and +//! sources the C++ runtime (`src/libcxx.rs`) and the ToyOS-hosted clang +//! (`src/hostedclang.rs`) are built from as its commit holds them ([`sources`]), +//! made by whichever build first needs it ([`resolve`]), and //! stored only when it was built from what the key names. Once its [`SOURCE`] //! file exists it is read-only, its directories as well as its files. //! Every compiler build names it as the host's `llvm-config` with @@ -39,7 +40,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::OnceLock; -use crate::buildlock::{Guard, Keyed}; +use crate::buildlock::{self, Guard, Held, Keyed}; use crate::compiler::LLVM; use crate::keystore::{self, Key}; use crate::sysroot::{clone_tree, git_bytes, git_out, gitlink}; @@ -50,7 +51,7 @@ use crate::toolchain::{self, host_triple}; const RECIPE: &str = "bootstrap build of src/llvm-project/llvm and src/llvm-project/lld; of the install, \ llvm-config, clang and llvm-ar in bin, and llvm-objcopy on an Apple host, LLVM's headers, \ every library llvm-config names and clang's resource headers; lld in bin, and the \ - runtimes' sources in src, read-only; 4"; + sources the C++ runtime and the ToyOS-hosted clang are built from in src, read-only; 5"; /// What of the caller's environment the LLVM build, and every tool its key /// asks, sees: @@ -71,7 +72,7 @@ const ENVIRONMENT: [&str; 2] = ["PATH", "TMPDIR"]; /// what a host has installed never decides what is built. Bootstrap's LLD step /// takes none of these: LLD looks for no library unless asked, and links what this /// LLVM chose. -const NO_HOST_LIBRARIES: [&str; 12] = [ +pub(crate) const NO_HOST_LIBRARIES: [&str; 12] = [ "LLVM_ENABLE_ZLIB", "LLVM_ENABLE_ZSTD", "LLVM_ENABLE_LIBXML2", @@ -127,7 +128,7 @@ pub fn host_lines(dir: &Path) -> String { /// changes no commit does. pub fn key(fork: &Path) -> Key { let tools = host_tools(); - key_of(fork, RECIPE, &config_text(Path::new(KEYED_BUILD_DIR), &host_triple(), tools, &stamp(fork)), &tools.identity) + key_of(fork, &recipe(), &config_text(Path::new(KEYED_BUILD_DIR), &host_triple(), tools, &stamp(fork)), &tools.identity) } /// What an LLVM, its clang and its LLD say they were built from. @@ -147,6 +148,37 @@ fn stamp(fork: &Path) -> Stamp { Stamp { revision: gitlink(fork, LLVM), repository: url.trim().to_string() } } +impl Stamp { + /// The CMake options that make LLVM say it. + fn options(&self) -> [(&'static str, &str); 2] { + [("LLVM_FORCE_VC_REVISION", &self.revision), ("LLVM_FORCE_VC_REPOSITORY", &self.repository)] + } +} + +/// The CMake options that make an LLVM built from `fork`'s say what it says +/// (`src/hostedclang.rs`). +pub(crate) fn stamp_options(fork: &Path) -> Vec<(String, String)> { + stamp(fork).options().iter().map(|(name, value)| (name.to_string(), value.to_string())).collect() +} + +/// The C and C++ compilers an LLVM is built with on this host, as its key +/// names them (`src/hostedclang.rs` builds its tablegens with them). +pub(crate) fn host_compilers() -> (PathBuf, PathBuf) { + let tools = host_tools(); + (tools.cc.clone(), tools.cxx.clone()) +} + +/// [`RECIPE`], with the pathspecs of what it keeps of the commit. +fn recipe() -> String { + format!("{RECIPE}\n{}", sources().join(" ")) +} + +/// What of the commit an LLVM keeps in `src/`, as git's pathspecs: what the C++ +/// runtime and the ToyOS-hosted clang are built from. +fn sources() -> Vec<&'static str> { + crate::libcxx::SOURCES.into_iter().chain(crate::hostedclang::SOURCES).collect() +} + /// [`key`], with what it reads beside `fork`'s committed `src/bootstrap`: /// `config` names the LLVM's commit ([`stamp`]). fn key_of(fork: &Path, recipe: &str, config: &str, tools: &str) -> Key { @@ -225,6 +257,41 @@ pub fn resolve(root: &Path, store: &Path, fork: &Path) -> Llvm { choose(store, fork, |fork| build_in_fork(root, fork)) } +/// [`resolve`] for a build holding its worktree's lock shared as `lock`: an +/// LLVM the store lacks is made with that lock held exclusively, so no other +/// build of the worktree runs in `fork` beside bootstrap's. +pub fn resolve_held(root: &Path, store: &Path, fork: &Path, lock: &mut Held) -> Llvm { + choose_held(store, fork, lock, defect, |fork| build_in_fork(root, fork)) +} + +/// [`resolve_held`] with the build passed in, as [`choose`] takes it, and what +/// decides whether the stored LLVM is whole, so a test can sweep the store +/// once it has answered. +fn choose_held( + store: &Path, + fork: &Path, + lock: &mut Held, + whole: impl Fn(&Path) -> Option, + build: impl Fn(&Path) -> PathBuf, +) -> Llvm { + let key = key(fork); + let dir = Keyed::Llvm.store(store).join(&key); + loop { + // Decided under the key's use, which no sweep takes from under it. + let using = buildlock::keyed_using(store, Keyed::Llvm, &key); + if whole(&dir).is_none() { + return Llvm { dir, _using: using }; + } + // The worktree lock orders before the key's. + drop(using); + let mut made = None; + lock.act_if("make the host's LLVM", || whole(&dir).map(drop), |()| made = Some(choose(store, fork, &build))); + if let Some(made) = made { + return made; + } + } +} + /// [`resolve`] with the build that makes an LLVM passed in, so a test can stand /// in for bootstrap: `build` builds in the fork checkout it is given and returns /// the build directory, holding `/llvm` and `/lld`. @@ -249,7 +316,7 @@ pub(crate) fn defect(dir: &Path) -> Option { .iter() .chain(&["lib/clang"]) .map(|k| dir.join(k)) - .chain(crate::libcxx::SOURCES.iter().map(|s| dir.join("src").join(s))); + .chain(sources().into_iter().filter(|s| !s.starts_with(':')).map(|s| dir.join("src").join(s))); let tools = tools().map(|t| dir.join("bin").join(t)).filter(|p| !p.is_file()); let gone: Vec = kept.filter(|p| !p.is_dir()).chain(tools).map(|p| p.display().to_string()).collect(); (!gone.is_empty()).then(|| format!("{} carries no {}", dir.display(), gone.join(", "))) @@ -302,7 +369,7 @@ fn place(fork: &Path, key: &Key, dir: &Path, build: &impl Fn(&Path) -> PathBuf) built_from.trim(), fork.display(), ); - check_out_committed(&checkout, &commit, &crate::libcxx::SOURCES, &partial.join("src")); + check_out_committed(&checkout, &commit, &sources(), &partial.join("src")); fs::write(partial.join(SOURCE), format!("{key}\n")) .unwrap_or_else(|e| panic!("write {}: {e}", partial.join(SOURCE).display())); read_only(&partial); @@ -356,7 +423,7 @@ fn libraries(install: &Path) -> Vec { /// reaches them. fn check_out_committed(checkout: &Path, commit: &str, paths: &[&str], dest: &Path) { fs::create_dir_all(dest).unwrap_or_else(|e| panic!("create {}: {e}", dest.display())); - let index = toyos_tmpdir::TempDir::new("llvm-runtimes-index"); + let index = toyos_tmpdir::TempDir::new("llvm-sources-index"); let out = Command::new("git") .env("GIT_INDEX_FILE", index.join("index")) .args(["-c", "core.sparseCheckout=false", "--work-tree"]) @@ -377,7 +444,7 @@ fn check_out_committed(checkout: &Path, commit: &str, paths: &[&str], dest: &Pat /// Take write permission from every file and directory under `dir`, and from /// `dir`. -fn read_only(dir: &Path) { +pub(crate) fn read_only(dir: &Path) { for entry in fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display())) { let path = entry.unwrap_or_else(|e| panic!("read {}: {e}", dir.display())).path(); let meta = fs::symlink_metadata(&path).unwrap_or_else(|e| panic!("stat {}: {e}", path.display())); @@ -454,8 +521,7 @@ fn build_in_fork(root: &Path, fork: &Path) -> PathBuf { /// host alone. fn config_text(build_dir: &Path, host: &str, tools: &HostTools, stamp: &Stamp) -> String { let off = NO_HOST_LIBRARIES.iter().map(|option| format!("{option} = \"OFF\"")); - let stamped = [("LLVM_FORCE_VC_REVISION", &stamp.revision), ("LLVM_FORCE_VC_REPOSITORY", &stamp.repository)]; - let defines: Vec = off.chain(stamped.map(|(option, value)| format!("{option} = \"{value}\""))).collect(); + let defines: Vec = off.chain(stamp.options().map(|(option, value)| format!("{option} = \"{value}\""))).collect(); format!( r#"change-id = "ignore" profile = "compiler" @@ -553,10 +619,10 @@ mod tests { if !checkout.join(".git").exists() { git(&checkout, &["init", "-q"]); } - write(&checkout.join("llvm/CMakeLists.txt"), content); - for source in crate::libcxx::SOURCES { + for source in sources().into_iter().filter(|s| !s.starts_with(':')) { write(&checkout.join(source).join("CMakeLists.txt"), &format!("the {source} of {content}")); } + write(&checkout.join("llvm/test/lit.cfg.py"), "what no build reads"); git(&checkout, &["add", "-A"]); let tree = git(&checkout, &["write-tree"]); let out = Command::new("git") @@ -729,7 +795,7 @@ mod tests { let tools = host_tools(); let config = config_text(Path::new(KEYED_BUILD_DIR), &host_triple(), tools, &stamp(&fork)); let base = key(&fork); - assert_eq!(key_of(&fork, RECIPE, &config, &tools.identity), base); + assert_eq!(key_of(&fork, &recipe(), &config, &tools.identity), base); let elsewhere = if host_triple() == "x86_64-unknown-linux-gnu" { "aarch64-unknown-linux-gnu" } else { @@ -738,9 +804,9 @@ mod tests { let elsewhere = config_text(Path::new(KEYED_BUILD_DIR), elsewhere, tools, &stamp(&fork)); for (what, other) in [ ("the recipe", key_of(&fork, "another recipe", &config, &tools.identity)), - ("the [llvm]", key_of(&fork, RECIPE, &config.replace("X86", "RISCV;X86"), &tools.identity)), - ("the host", key_of(&fork, RECIPE, &elsewhere, &tools.identity)), - ("the host's tools", key_of(&fork, RECIPE, &config, "/usr/bin/gcc\ngcc 14\n")), + ("the [llvm]", key_of(&fork, &recipe(), &config.replace("X86", "RISCV;X86"), &tools.identity)), + ("the host", key_of(&fork, &recipe(), &elsewhere, &tools.identity)), + ("the host's tools", key_of(&fork, &recipe(), &config, "/usr/bin/gcc\ngcc 14\n")), ] { assert_ne!(other, base, "{what} did not move the key"); } @@ -799,7 +865,7 @@ mod tests { "--show-sdk-version" => format!("{version}\n"), other => panic!("xcrun was asked {other}"), }); - key_of(&fork, RECIPE, &config_text(Path::new(KEYED_BUILD_DIR), &host_triple(), &tools, &stamp(&fork)), &tools.identity) + key_of(&fork, &recipe(), &config_text(Path::new(KEYED_BUILD_DIR), &host_triple(), &tools, &stamp(&fork)), &tools.identity) }); assert_ne!(older, newer, "two SDK versions at one SDK path named one LLVM"); } @@ -993,11 +1059,13 @@ mod tests { assert!(stored.iter().all(|n| n.to_string_lossy().ends_with(".partial")), "stored: {stored:?}"); } - /// **The runtimes' sources are the commit's**: made while the LLVM was + /// **The sources it keeps are the commit's**: made while the LLVM was /// built, an edit to a file the commit holds is refused and nothing is - /// stored, and a file the checkout ignores is not stored with it. + /// stored, a file the checkout ignores is not stored with it, and the + /// ToyOS-hosted clang's are kept beside the runtimes' without the tests + /// its pathspecs leave out. #[test] - fn the_runtimes_sources_are_the_commit_s() { + fn the_kept_sources_are_the_commit_s() { let scratch = Scratch::new("llvm-runtimes"); let (_primary, store, [_same, a, _b]) = estate_built(&scratch); let fork = a.join("rust"); @@ -1022,6 +1090,9 @@ mod tests { assert_eq!(fs::read_to_string(dir.join("src/libcxx/CMakeLists.txt")).unwrap(), "the libcxx of A"); assert!(checkout.join("libcxx/utils/cache.pyc").is_file()); assert!(!dir.join("src/libcxx/utils").exists(), "a file the checkout ignores was stored"); + assert_eq!(fs::read_to_string(dir.join("src/clang/CMakeLists.txt")).unwrap(), "the clang of A"); + assert!(checkout.join("llvm/test/lit.cfg.py").is_file()); + assert!(!dir.join("src/llvm/test").exists(), "a test the pathspecs leave out was stored"); } const WORKTREE: &str = "TOYOS_LLVM_TEST_WORKTREE"; @@ -1079,6 +1150,58 @@ mod tests { assert_eq!(defect(&choose(&store, &b.join("rust"), never).dir), None); } + /// **An LLVM is made only with the worktree lock exclusive, and one the + /// store holds is found under the shared lock**: while `a`'s is made, a + /// shared acquirer of `a`'s lock is kept out; `b`, whose worktree another + /// build holds shared, finds that LLVM without waiting for it to finish. + #[test] + fn an_llvm_is_made_only_with_the_worktree_lock_exclusive() { + let scratch = Scratch::new("llvm-held"); + let (_primary, store, [_same, a, b]) = estate_built(&scratch); + let mut lock = buildlock::shared(&a, "the test's build"); + let makes = Cell::new(0); + let alone = |fork: &Path| { + makes.set(makes.get() + 1); + assert!(buildlock::tests::keeps_out_shared(&a), "an LLVM was built in {} beside shared holders", fork.display()); + fake_build(fork) + }; + let made = choose_held(&store, &a.join("rust"), &mut lock, defect, alone); + assert_eq!(makes.get(), 1); + assert_eq!(defect(&made.dir), None); + + let other = buildlock::tests::shared_elsewhere(&b); + let mut lock = buildlock::shared(&b, "the test's build"); + let never = |_: &Path| -> PathBuf { panic!("an LLVM the store holds was made again") }; + assert_eq!(choose_held(&store, &b.join("rust"), &mut lock, defect, never).dir, made.dir); + other.release(); + } + + /// **A sweep that lands once a held build has found its LLVM whole fails no + /// build**: the stored LLVM, unused for the keep time, is swept by a + /// placement elsewhere right after it is found whole, and the build goes on + /// with a whole LLVM. + #[test] + fn a_sweep_after_the_llvm_is_found_whole_fails_no_build() { + let scratch = Scratch::new("llvm-held-swept"); + let (_primary, store, [_same, a, _b]) = estate_built(&scratch); + let fork = a.join("rust"); + drop(choose(&store, &fork, fake_build)); + let unused = key(&fork); + let swept = Cell::new(None); + let then_swept = |dir: &Path| { + let found = defect(dir); + if swept.get().is_none() { + last_used(&store, Keyed::Llvm, &unused, LONG_AGO); + swept.set(Some(keystore::sweep(&store, Keyed::Llvm).len())); + } + found + }; + let mut lock = buildlock::shared(&a, "the test's build"); + let llvm = choose_held(&store, &fork, &mut lock, then_swept, fake_build); + assert_eq!(defect(&llvm.dir), None, "the build went on with an LLVM that is not whole"); + assert!(swept.get().is_some(), "the stand-in sweep never ran"); + } + /// **A sweep takes an LLVM only once nothing has used it for the store's /// keep time and nobody uses it**: `a` moves to another LLVM while a /// process of its own still uses the first, which placing the second diff --git a/src/main.rs b/src/main.rs index 9498e8be192..50e84e100d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,6 +107,11 @@ fn main() { toyos_build::clippy::dispatch(&root); return; } + // Builds no image, and runs no guest. + if asked(&flags::HOSTED_CLANG) { + toyos_build::hostedclang::dispatch(&root); + return; + } // Writes one file outside the checkout and builds nothing. if asked(&flags::SIGNING_KEY_NEW) { match toyos_build::signing::mint_owner_key() { @@ -214,11 +219,6 @@ fn main() { return; } - if asked(&flags::REGEN_WALLPAPER) { - toyos_build::wallpaper::regen(&root); - return; - } - if let Some(bank) = CARGO_RUN.value(&args, &flags::REGEN_SOUNDFONT) { toyos_build::soundfont::regen(&root, Path::new(bank)); return; diff --git a/src/metal.rs b/src/metal.rs index 2602dd15bdb..3592cddd51d 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -2872,38 +2872,27 @@ mod tests { /// A disk carrying `parts` in order, so `admit`'s refusals are exercised on /// tables rather than on prose about tables. - fn disk(parts: &[(&str, &gpt::partition_types::Type)]) -> Vec { - use std::io::Cursor; - const TOTAL: usize = 16 * 1024 * 1024; - let mut bytes = vec![0u8; TOTAL]; - let mut cursor = Cursor::new(&mut bytes); - gpt::mbr::ProtectiveMBR::with_lb_size((TOTAL / 512 - 1) as u32) - .overwrite_lba0(&mut cursor) - .expect("the protective MBR"); - let mut gdisk = gpt::GptConfig::default() - .initialized(false) - .writable(true) - .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) - .create_from_device(Box::new(cursor), None) - .expect("a table"); - gdisk.update_partitions(std::collections::BTreeMap::new()).expect("an empty table"); - for (name, kind) in parts { - gdisk - .add_partition(name, 256 * 1024, (*kind).clone(), 0, Some(2048)) - .expect("a partition"); - } - let mut device = gdisk.write().expect("the table"); - device.seek(SeekFrom::Start(0)).expect("rewind"); - let mut out = vec![0u8; TOTAL]; - device.read_exact(&mut out).expect("read back"); + fn disk(parts: &[(&str, toyos_gpt::Guid)]) -> Vec { + const TOTAL: u64 = 16 * 1024 * 1024; + const ALIGN: u64 = 2048; + let entries: Vec> = (1..) + .zip(parts) + .map(|(i, (name, kind))| crate::gptwrite::Entry { + kind: *kind, + unique: crate::gptwrite::random_guid(), + first_lba: i * ALIGN, + last_lba: i * ALIGN + 511, + name, + }) + .collect(); + let table = crate::gptwrite::table(TOTAL / u64::from(LBA), crate::gptwrite::random_guid(), &entries); + let mut out = vec![0u8; TOTAL as usize]; + out[..table.primary.len()].copy_from_slice(&table.primary); + let back = out.len() - table.backup.len(); + out[back..].copy_from_slice(&table.backup); out } - const TOYOS_ROOT: gpt::partition_types::Type = gpt::partition_types::Type { - guid: toyos_gpt::Guid::TOYOS_ROOT_TEXT, - os: gpt::partition_types::OperatingSystem::None, - }; - #[test] fn an_image_admits_only_the_table_the_installed_rule_names() { let dir = toyos_tmpdir::TempDir::new("metal"); @@ -2913,8 +2902,7 @@ mod tests { std::fs::write(&at, bytes).expect("write"); at }; - let esp = gpt::partition_types::EFI; - let basic = gpt::partition_types::BASIC; + let (esp, basic, root) = (toyos_gpt::Guid::EFI_SYSTEM, toyos_gpt::Guid::MICROSOFT_BASIC, toyos_gpt::Guid::TOYOS_ROOT); assert_eq!( admit(&write("ragged.img", &vec![0u8; 1000]), &t), @@ -2934,24 +2922,24 @@ mod tests { Err(Refusal::BackupHeader { .. }) )); - let two = disk(&[("a", &esp), ("b", &esp), ("log", &basic), ("root", &TOYOS_ROOT)]); + let two = disk(&[("a", esp), ("b", esp), ("log", basic), ("root", root)]); assert_eq!( admit(&write("two-esps.img", &two), &t), Err(Refusal::Partitions { what: "ESP", matched: 2 }) ); - let no_log = disk(&[("esp", &esp), ("root", &TOYOS_ROOT)]); + let no_log = disk(&[("esp", esp), ("root", root)]); assert_eq!( admit(&write("no-log.img", &no_log), &t), Err(Refusal::Partitions { what: "TOYOS-LOG", matched: 0 }) ); // The log where the installed rule does not name it: p2, not p3. - let moved = disk(&[("esp", &esp), ("log", &basic), ("root", &TOYOS_ROOT)]); + let moved = disk(&[("esp", esp), ("log", basic), ("root", root)]); assert_eq!( admit(&write("moved-log.img", &moved), &t), Err(Refusal::PartitionIndex { what: "TOYOS-LOG", want: 3, got: 2 }) ); // And the shape `src/image.rs` writes, which is the one admitted. - let built = disk(&[("esp", &esp), ("root", &TOYOS_ROOT), ("log", &basic)]); + let built = disk(&[("esp", esp), ("root", root), ("log", basic)]); let ok = admit(&write("built.img", &built), &t).expect("the built shape is admitted"); assert_eq!((ok.esp.index, ok.log.index), (1, 3)); assert_eq!(ok.bytes % u64::from(LBA), 0); diff --git a/src/release.rs b/src/release.rs index e12d7c8afa7..7ad11f29d8f 100644 --- a/src/release.rs +++ b/src/release.rs @@ -46,17 +46,39 @@ const USER_AGENT: &str = "toyos-build (https://github.com/ToyOSOrg/ToyOS)"; /// The products a toolchain is, in the order a build makes them, each under the /// name its cache entry and its job's outputs carry. -const LAYERS: [(Keyed, &str); 4] = [ - (Keyed::Llvm, "llvm"), - (Keyed::Compiler, "compiler"), - (Keyed::Freestanding, "freestanding"), - (Keyed::Sysroot, "sysroot"), +const LAYERS: [(Part, &str); 4] = [ + (Part::Llvm, "llvm"), + (Part::Compiler, "compiler"), + (Part::Freestanding, "freestanding"), + (Part::Sysroot, "sysroot"), ]; +/// What a toolchain is made of: the products every build reads, of the kinds +/// the build system keys. The ToyOS-hosted clang is none, made only when asked +/// for (`src/hostedclang.rs`). +#[derive(Clone, Copy)] +enum Part { + Llvm, + Compiler, + Freestanding, + Sysroot, +} + +impl Part { + fn keyed(self) -> Keyed { + match self { + Part::Llvm => Keyed::Llvm, + Part::Compiler => Keyed::Compiler, + Part::Freestanding => Keyed::Freestanding, + Part::Sysroot => Keyed::Sysroot, + } + } +} + /// One of [`LAYERS`] of this tree's toolchain: the key the build system files /// it under, and where it is, relative to the checkout. struct Layer { - kind: Keyed, + kind: Part, name: &'static str, key: Key, path: PathBuf, @@ -130,12 +152,12 @@ fn layers(root: &Path) -> Vec { .iter() .map(|&(kind, name)| { let key = match kind { - Keyed::Llvm => &llvm, - Keyed::Compiler => &compiler, - Keyed::Freestanding => &freestanding, - Keyed::Sysroot => &sysroot, + Part::Llvm => &llvm, + Part::Compiler => &compiler, + Part::Freestanding => &freestanding, + Part::Sysroot => &sysroot, }; - let dir = kind.store(&store(root)).join(key); + let dir = kind.keyed().store(&store(root)).join(key); let path = dir.strip_prefix(root).expect("a runner's store is in its checkout").to_path_buf(); Layer { kind, name, key: key.clone(), path } }) @@ -147,12 +169,12 @@ fn layers(root: &Path) -> Vec { fn defect(root: &Path, layer: &Layer) -> Option { let dir = root.join(&layer.path); match layer.kind { - Keyed::Llvm => crate::llvm::defect(&dir), - Keyed::Compiler => { + Part::Llvm => crate::llvm::defect(&dir), + Part::Compiler => { crate::compiler::unplaced(&dir).or_else(|| crate::toolchain::toolchain_defect(&dir.join("stage2"))) } - Keyed::Freestanding => crate::sysroot::unpublished(&dir), - Keyed::Sysroot => crate::sysroot::unfinished(&dir), + Part::Freestanding => crate::sysroot::unpublished(&dir), + Part::Sysroot => crate::sysroot::unfinished(&dir), } } diff --git a/src/wallpaper.rs b/src/wallpaper.rs index 90cba2a336f..3b0b52b84da 100644 --- a/src/wallpaper.rs +++ b/src/wallpaper.rs @@ -1,27 +1,20 @@ -//! The desktop background, written down as a description instead of fetched. -//! -//! `assets/wallpaper.jpg` used to be a file of unknown origin — an aggregator -//! upload naming no author and no copyright holder, under a "wallpaper use -//! only" restriction nobody on that page had the standing to grant. What -//! replaces it is the output of [`draw`], so provenance is answered by source -//! anyone can re-run rather than by a paragraph in `NOTICE`. +//! The desktop background, drawn by the build: what the compositor reads as +//! `/system/share/wallpaper.rgb` is [`draw`]'s output, byte for byte, so its +//! provenance is source anyone can re-run and no file stands between the two. //! //! The picture is a lit horizon: a night sky, a glow behind it, and three -//! ridges receding into haze. It is nothing but the constants below — change -//! one, run `cargo run -- --regen-wallpaper`, and the committed artifact moves -//! with it. +//! ridges receding into haze. It is nothing but the constants below. //! //! All compositing is in **linear light**, converted to sRGB once per pixel at //! the end. Mixing two colours in sRGB mixes two numbers that were never linear //! to begin with, which is what sends a naive gradient grey through its middle. +//! +//! **The bytes are the same on every host**, because ROOT's UUID is derived +//! from them: every transcendental is `libm`'s, never the host C library's, and +//! `the_wallpaper_is_the_same_on_every_host` pins their digest. -use std::path::Path; - -use image::codecs::jpeg::JpegEncoder; -use image::ExtendedColorType; - -/// Where the artifact lives, relative to the repository root. -pub const WALLPAPER_PATH: &str = "assets/wallpaper.jpg"; +/// The wallpaper's name in ROOT's `share/`. +pub const NAME: &str = "wallpaper.rgb"; /// Drawn at the panel this project targets, the T14's 1920x1080. The /// compositor scales whatever it is handed, so this is the size at which it @@ -29,20 +22,6 @@ pub const WALLPAPER_PATH: &str = "assets/wallpaper.jpg"; pub const WIDTH: u32 = 1920; pub const HEIGHT: u32 = 1080; -/// **The quantization table is why this is up here and not at the usual 90.** -/// libjpeg's scaling leaves every entry of both tables at 1 by 99, and `image`'s -/// encoder never subsamples chroma (`h: 1, v: 1` on all three components), so -/// the round trip is the DCT's own rounding and nothing else. Anywhere below -/// that the DC coefficient alone is quantized in steps of two or three levels, -/// which lays an 8x8 staircase across exactly the smooth field this picture is -/// made of — and the same quantizer crushes the dither that was there to stop -/// one. `the_wallpaper_neither_bands_nor_blocks` is that stated as a bound. -/// -/// Not 100, which is the same file size to within 0.2% and worse on every other -/// measure. Both readings come from a sweep of the encoder across the quality -/// range at this picture's own dimensions. -pub const QUALITY: u8 = 99; - /// The sky, top to horizon, as sRGB. /// /// Both nearly black on purpose. The glow is what the eye sees; a sky bright @@ -168,7 +147,7 @@ const RIDGE_OCTAVES: u32 = 4; /// span the screen. /// /// This is the difference between a sky and a diagram of three ellipses. Both -/// octaves are slow: anything faster costs JPEG bits and reads as texture. +/// octaves are slow: anything faster reads as texture. const DRIFT: f32 = 0.42; const DRIFT_CELLS: f32 = 2.4; @@ -184,7 +163,7 @@ const VIGNETTE_OUT: f32 = 1.25; /// This is dither and not decoration. Eight bits a channel over a field this /// smooth puts a contour every few dozen rows; a triangular perturbation of /// this size turns the contour into noise the eye integrates away, and it is -/// what lets `the_wallpaper_neither_bands_nor_blocks` hold. +/// what lets `the_wallpaper_does_not_band` hold. const GRAIN: f32 = 1.6; fn srgb_to_linear(c: u8) -> f32 { @@ -192,7 +171,7 @@ fn srgb_to_linear(c: u8) -> f32 { if s <= 0.040_45 { s / 12.92 } else { - ((s + 0.055) / 1.055).powf(2.4) + libm::powf((s + 0.055) / 1.055, 2.4) } } @@ -201,7 +180,7 @@ fn linear_to_srgb(v: f32) -> f32 { if v <= 0.003_130_8 { v * 12.92 } else { - 1.055 * v.powf(1.0 / 2.4) - 0.055 + 1.055 * libm::powf(v, 1.0 / 2.4) - 0.055 } } @@ -315,7 +294,7 @@ pub fn draw(width: u32, height: u32) -> Vec { let dx = (u - light.x) / light.rx; let dy = (v - light.y) / light.ry; let warped = (dx * dx + dy * dy) * (1.0 + DRIFT * drift(u, v, light.seed)); - let fall = (-warped).exp() * light.gain; + let fall = libm::expf(-warped) * light.gain; for c in 0..3 { rgb[c] += color[c] * fall; } @@ -353,25 +332,17 @@ pub fn draw(width: u32, height: u32) -> Vec { out } -/// [`draw`] at [`WIDTH`]x[`HEIGHT`], JPEG-encoded at [`QUALITY`]. -pub fn encoded() -> Vec { - let rgb = draw(WIDTH, HEIGHT); - let mut jpeg = Vec::new(); - JpegEncoder::new_with_quality(&mut jpeg, QUALITY) - .encode(&rgb, WIDTH, HEIGHT, ExtendedColorType::Rgb8) - .expect("encode the wallpaper"); - jpeg +/// The file ROOT carries as `share/`[`NAME`]: the width and the height as +/// little-endian `u32`s, then [`draw`]'s RGB triples at that size, unencoded — +/// a lossy codec here would throw away the dither first. +pub fn rgb() -> Vec { + let mut file = Vec::with_capacity(8 + WIDTH as usize * HEIGHT as usize * 3); + file.extend(WIDTH.to_le_bytes()); + file.extend(HEIGHT.to_le_bytes()); + file.extend(draw(WIDTH, HEIGHT)); + file } -/// Rewrite `assets/wallpaper.jpg` from the constants in this file. -pub fn regen(root: &Path) { - let jpeg = encoded(); - let path = root.join(WALLPAPER_PATH); - std::fs::write(&path, &jpeg).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); - println!("wrote {} ({} bytes)", path.display(), jpeg.len()); -} - - #[cfg(test)] mod tests { use super::*; @@ -386,15 +357,6 @@ mod tests { /// almost four times it. const MAX_FLAT_RUN: usize = 40; - /// How much more a pixel may differ from the one above it across an 8x8 - /// block boundary than inside one. - /// - /// JPEG's characteristic artifact *is* this ratio going up — the blocks are - /// quantized independently, so what a person sees as blocking is a step - /// that appears only where two of them meet. A picture with none of it - /// sits at 1.0, and the value this bound separates from is a doubling. - const MAX_BLOCKING: f64 = 1.25; - fn longest_column_run(rgb: &[u8], width: usize, height: usize, channel: usize) -> usize { let mut worst = 0; for x in 0..width { @@ -409,71 +371,29 @@ mod tests { worst } - fn blocking(rgb: &[u8], width: usize, height: usize) -> f64 { - let (mut across, mut across_n) = (0.0f64, 0u64); - let (mut within, mut within_n) = (0.0f64, 0u64); - for y in 1..height { - for x in 0..width { - for c in 0..3 { - let step = (rgb[(y * width + x) * 3 + c] as i32 - - rgb[((y - 1) * width + x) * 3 + c] as i32) - .abs() as f64; - if y % 8 == 0 { - across += step; - across_n += 1; - } else { - within += step; - within_n += 1; - } - } - } - } - (across / across_n as f64) / (within / within_n as f64) + /// The file that ships is the one every host draws: a host whose floating + /// point answers differently reds here rather than naming a different ROOT. + #[test] + fn the_wallpaper_is_the_same_on_every_host() { + let digest: String = toyos_sha2_hw::sha256_digest(rgb()).iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!(digest, "675f4fbc92985dc5e60b899fd869fa8e574b1147e2b1c9b42808a72bcfff1d91"); } - /// What ships is the decoded JPEG, so the decoded JPEG is what is measured. - /// - /// Asking either question of [`draw`]'s own output would pass at an encoder - /// setting that undoes both: the dither is the highest-frequency thing in - /// the picture and therefore the first thing a quantizer throws away, and - /// the staircase it was holding off comes back as blocking. Between the two - /// they pin the encoder as well as the drawing — nothing here can be - /// weakened without one of them saying so. + /// The file that ships, read the way the compositor reads it, does not band. #[test] - fn the_wallpaper_neither_bands_nor_blocks() { - let decoded = image::load_from_memory_with_format(&encoded(), image::ImageFormat::Jpeg) - .expect("decode what we just encoded") - .to_rgb8(); - let (w, h) = (decoded.width() as usize, decoded.height() as usize); + fn the_wallpaper_does_not_band() { + let file = rgb(); + let (head, pixels) = file.split_at(8); + let w = u32::from_le_bytes(head[..4].try_into().unwrap()) as usize; + let h = u32::from_le_bytes(head[4..].try_into().unwrap()) as usize; + assert_eq!(pixels.len(), w * h * 3, "a {w}x{h} wallpaper of {} bytes", pixels.len()); for (channel, name) in ["red", "green", "blue"].iter().enumerate() { - let run = longest_column_run(decoded.as_raw(), w, h, channel); + let run = longest_column_run(pixels, w, h, channel); assert!( run <= MAX_FLAT_RUN, "{run} rows of one {name} value down a column, over a bound of \ {MAX_FLAT_RUN}: this wallpaper bands" ); } - let blocking = blocking(decoded.as_raw(), w, h); - assert!( - blocking <= MAX_BLOCKING, - "steps across the 8x8 block boundaries are {blocking:.2}x the steps inside them, \ - over a bound of {MAX_BLOCKING}: QUALITY = {QUALITY} is not enough for a field \ - this smooth" - ); - } - - /// The committed file is [`draw`]'s output and not a picture somebody put - /// there — the property this whole module exists to establish, and the - /// answer to the licence question that replacing it was about. - #[test] - fn the_committed_wallpaper_is_the_one_this_file_describes() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")); - let on_disk = std::fs::read(root.join(WALLPAPER_PATH)).expect("read the wallpaper"); - assert!( - on_disk == encoded(), - "{WALLPAPER_PATH} is {} bytes and is not what this file draws — run \ - `cargo run -- --regen-wallpaper`", - on_disk.len() - ); } } diff --git a/tests/libc-arch/src/alarm_requests.rs b/tests/libc-arch/src/alarm_requests.rs new file mode 100644 index 00000000000..e82db69a011 --- /dev/null +++ b/tests/libc-arch/src/alarm_requests.rs @@ -0,0 +1,41 @@ +//! What `alarm` answers and arms: the seconds an earlier alarm has left, +//! rounded up and 0 once it is due or when there is none; 0 seconds arm +//! nothing; and a due alarm ends the process unless `SIGALRM` is ignored, the +//! signal `signal.h` numbers. + +use crate::alarmreq::{due, ends, left, SIGALRM, SIG_DFL, SIG_IGN}; +use crate::header::{self, SIGNAL_H}; + +const SEC: u64 = 1_000_000_000; + +#[test] +fn an_alarm_is_due_its_seconds_after_it_is_armed_and_zero_arms_none() { + assert_eq!(due(7, 0), None); + assert_eq!(due(7, 1), Some(7 + SEC)); + assert_eq!(due(u64::from(u32::MAX), u32::MAX), Some(u64::from(u32::MAX) * (SEC + 1))); +} + +#[test] +fn the_seconds_left_round_up_and_end_at_zero() { + let armed = due(100, 5); + assert_eq!(left(armed, 100), 5); + assert_eq!(left(armed, 100 + 1), 5, "a second begun is a second left"); + assert_eq!(left(armed, 100 + SEC), 4); + assert_eq!(left(armed, 100 + 5 * SEC - 1), 1, "an alarm not yet due answers at least 1"); + assert_eq!(left(armed, 100 + 5 * SEC), 0, "an alarm due answers 0"); + assert_eq!(left(armed, 100 + 6 * SEC), 0, "an alarm past due answers 0"); + assert_eq!(left(None, 100), 0); + assert_eq!(left(due(0, u32::MAX), 0), u32::MAX); +} + +#[test] +fn a_due_alarm_ends_the_process_unless_sigalrm_is_ignored() { + assert!(!ends(SIG_IGN), "an ignored SIGALRM ended the process"); + assert!(ends(SIG_DFL)); + assert!(ends(0x40_1000), "a handler, which never runs, kept the process"); +} + +#[test] +fn sigalrm_is_the_number_signal_h_gives_it() { + assert_eq!(SIGALRM, header::int(SIGNAL_H, "SIGALRM")); +} diff --git a/tests/libc-arch/src/fparts_differential.rs b/tests/libc-arch/src/fparts_differential.rs index 43f513e8c6a..6b9b1433489 100644 --- a/tests/libc-arch/src/fparts_differential.rs +++ b/tests/libc-arch/src/fparts_differential.rs @@ -1,12 +1,17 @@ -//! `modf` and `logb` against the host C library's, bit for bit: every special -//! value, the edges of the subnormals and of the integers a double holds, and -//! a spread of bit patterns across every exponent. +//! `modf`, `round`, `lround` and `logb` against the host C library's, bit for +//! bit: every special value, the edges of the subnormals, of the halves and of +//! the integers a double and a `long` hold, and a spread of bit patterns across +//! every exponent. use crate::fparts; extern "C" { #[link_name = "modf"] fn host_modf(x: f64, int: *mut f64) -> f64; + #[link_name = "round"] + fn host_round(x: f64) -> f64; + #[link_name = "lround"] + fn host_lround(x: f64) -> i64; #[link_name = "logb"] fn host_logb(x: f64) -> f64; } @@ -30,9 +35,15 @@ fn inputs() -> Vec { f64::MIN_POSITIVE / 2.0, f64::from_bits(1), f64::from_bits(0x000f_ffff_ffff_ffff), + 0.49999999999999994, + 3.5, 4_503_599_627_370_495.5, 4_503_599_627_370_496.0, 9_007_199_254_740_993.0, + // The last double below 2^63, 2^63 itself, and -2^63. + 9_223_372_036_854_774_784.0, + 9_223_372_036_854_775_808.0, + -9_223_372_036_854_775_808.0, f64::INFINITY, f64::NAN, ]; @@ -62,6 +73,37 @@ fn modf_is_the_host_libraries() { } } +#[test] +fn round_is_the_host_libraries() { + for x in inputs() { + // SAFETY: a pure function of its argument. + let host = unsafe { host_round(x) }; + let ours = fparts::round(x); + assert!(same(ours, host), "round({x:e} = {:#x}): {ours:e}, the host {host:e}", x.to_bits()); + } +} + +/// Where the rounding is a `long`, the host's; where it is not, a NaN among +/// them, the domain error, whose value the host leaves unspecified. +#[test] +fn lround_is_the_host_libraries_and_out_of_range_its_domain_error() { + let mut refused = 0; + for x in inputs() { + // SAFETY: a pure function of its argument. + let rounded = unsafe { host_round(x) }; + let fits = (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&rounded); + match fparts::lround(x) { + // SAFETY: a pure function of its argument, here inside its domain. + Ok(ours) => assert!(fits && ours == unsafe { host_lround(x) }, "lround({x:e}): {ours}"), + Err(fparts::Domain) => { + assert!(!fits, "lround({x:e}) refused a rounding {rounded:e} that is a long"); + refused += 1; + } + } + } + assert!(refused > 0, "no input was outside a long"); +} + #[test] fn logb_is_the_host_libraries_and_a_zero_its_pole() { for x in inputs() { diff --git a/tests/libc-arch/src/lib.rs b/tests/libc-arch/src/lib.rs index fa2c1def040..0fe3d7e9f89 100644 --- a/tests/libc-arch/src/lib.rs +++ b/tests/libc-arch/src/lib.rs @@ -14,6 +14,9 @@ #[cfg(test)] extern crate alloc; +#[cfg(test)] +#[path = "../../../userland/libc/src/alarmreq.rs"] +mod alarmreq; #[cfg(test)] #[path = "../../../userland/libc/src/arch/mod.rs"] mod arch; @@ -60,6 +63,8 @@ mod text; #[path = "../../../userland/libc/src/utf8.rs"] mod utf8; +#[cfg(test)] +mod alarm_requests; #[cfg(test)] mod descriptor_requests; #[cfg(test)] diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 8bfc4d2ed5d..8e59332f71b 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -1,7 +1,7 @@ /* What libc refuses: each call answers failure in its own POSIX form, errno - says why, and nothing is done: ENOSYS where ToyOS lacks the function, and - the errno POSIX names for a lock or a mapping it cannot take, or memory it - cannot give. */ + says why, and nothing is done: ENOSYS where ToyOS lacks the function, ECHILD + for a wait on the child libc cannot start, and the errno POSIX names for a + lock, a limit or a mapping it cannot take, or memory it cannot give. */ #include #include #include @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #define FILE_PATH "/tmp/206_libc_refusals" @@ -19,6 +21,7 @@ static const char *errno_name(int e) { switch (e) { case 0: return "no errno"; + case ECHILD: return "ECHILD"; case ENOSYS: return "ENOSYS"; case EINVAL: return "EINVAL"; case ENODEV: return "ENODEV"; @@ -81,6 +84,11 @@ int main(void) { said("msync", msync(page, 4096, MS_SYNC)); said("mprotect", mprotect(page, 4096, PROT_READ)); said("realpath", realpath("/tmp", NULL) == NULL ? -1 : 0); + said("utimes", utimes(FILE_PATH, NULL)); + int status = 0; + said("wait", wait(&status)); + said("wait4", wait4(-1, &status, 0, NULL)); + said("pathconf 12345", pathconf(FILE_PATH, 12345)); said("fcntl F_SETLK", fcntl(fd, F_SETLK, &lock)); said("fcntl F_SETLKW", fcntl(fd, F_SETLKW, &lock)); said("fcntl F_GETLK", fcntl(fd, F_GETLK, &lock)); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index daf32c2d78e..dc7daa9f70b 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -15,6 +15,10 @@ setrlimit: -1, ENOSYS msync: -1, ENOSYS mprotect: -1, ENOSYS realpath: -1, ENOSYS +utimes: -1, ENOSYS +wait: -1, ECHILD +wait4: -1, ECHILD +pathconf 12345: -1, EINVAL fcntl F_SETLK: -1, EINVAL fcntl F_SETLKW: -1, EINVAL fcntl F_GETLK: -1, EINVAL diff --git a/tests/testcases/tinycc/207_libc_names.c b/tests/testcases/tinycc/207_libc_names.c index 201f70b2716..70512871cfb 100644 --- a/tests/testcases/tinycc/207_libc_names.c +++ b/tests/testcases/tinycc/207_libc_names.c @@ -1,7 +1,9 @@ /* What libc does for the names LLVM builds against: a directory listed with the name and kind of each entry; dladdr naming the image and the exported - symbol an address lies in; and strnlen, strsignal, modf, logb and the - conversions. */ + symbol an address lies in; a file truncated by its path; a stream on a + buffer of its caller's, or none, sought and told by off_t; SIGALRM's + action, read back whole, and an alarm armed and disarmed; and strnlen, strsignal, modf, + lround, logb, pathconf and the conversions. */ #include #include #include @@ -46,6 +48,7 @@ int main(void); static const char *errno_name(int e) { switch (e) { + case EDOM: return "EDOM"; case EEXIST: return "EEXIST"; case EINVAL: return "EINVAL"; case ENOENT: return "ENOENT"; @@ -85,6 +88,64 @@ static void links(void) { refused("readlink of nothing", readlink(DIR_PATH "/none", buf, sizeof buf)); } +static void truncated(void) { + struct stat st; + int answer = truncate(TARGET, 2); + printf("truncate to 2: %d; stat size: %ld\n", answer, stat(TARGET, &st) == 0 ? (long)st.st_size : -1L); + refused("truncate of nothing", truncate(DIR_PATH "/none", 0)); +} + +/* setbuf's buffer holds what is written until the stream is told or sought; + with none, a write reaches the file at once. */ +static void buffered(void) { + static char buf[BUFSIZ]; + struct stat st; + FILE *f = fopen(DIR_PATH "/buffered", "w+"); + if (!f) { + printf("fopen buffered failed\n"); + return; + } + setbuf(f, buf); + fputs("0123456789", f); + long held = stat(DIR_PATH "/buffered", &st) == 0 ? (long)st.st_size : -1L; + printf("setbuf: file %ld bytes, the buffer %s\n", held, memcmp(buf, "0123456789", 10) == 0 ? "holds them" : "not"); + long told = (long)ftello(f); + int sought = fseeko(f, 3, SEEK_SET); + long at = (long)ftello(f); + int c = fgetc(f); + printf("ftello: %ld; fseeko 3: %d; ftello: %ld; fgetc: %c\n", told, sought, at, c); + fclose(f); + + f = fopen(DIR_PATH "/unbuffered", "w"); + if (!f) { + printf("fopen unbuffered failed\n"); + return; + } + setbuf(f, NULL); + fputs("abc", f); + printf("setbuf NULL: file %ld bytes\n", stat(DIR_PATH "/unbuffered", &st) == 0 ? (long)st.st_size : -1L); + fclose(f); +} + +static void alarms(void) { + void (*was)(int) = signal(SIGALRM, SIG_IGN); + void (*then)(int) = signal(SIGALRM, SIG_DFL); + printf("signal SIGALRM: was %s, then %s\n", was == SIG_DFL ? "SIG_DFL" : "another", + then == SIG_IGN ? "SIG_IGN" : "another"); + struct sigaction ignore = { .sa_handler = SIG_IGN, .sa_flags = SA_RESTART }, old = { .sa_handler = SIG_DFL }; + sigemptyset(&ignore.sa_mask); + sigaddset(&ignore.sa_mask, SIGUSR1); + int set = sigaction(SIGALRM, &ignore, NULL); + int got = sigaction(SIGALRM, NULL, &old); + printf("sigaction SIGALRM: %d %d, %s, flags 0x%lx, mask 0x%lx\n", set, got, + old.sa_handler == SIG_IGN ? "SIG_IGN" : "another", old.sa_flags, (unsigned long)old.sa_mask); + signal(SIGALRM, SIG_DFL); + unsigned first = alarm(5); + unsigned left = alarm(0); + unsigned again = alarm(0); + printf("alarm 5: %u; alarm 0: %s; again: %u\n", first, left >= 1 && left <= 5 ? "1 to 5" : "another", again); +} + static void listing(void) { if (mkdir(LISTED, 0755) != 0 || mkdir(LISTED "/sub", 0755) != 0 || close(creat(LISTED "/a", 0644)) != 0 || close(creat(LISTED "/b", 0644)) != 0 || close(creat(LISTED "/sub/inner", 0644)) != 0) { @@ -154,6 +215,9 @@ int main(void) { return 1; } links(); + truncated(); + buffered(); + alarms(); listing(); dl(); @@ -166,6 +230,13 @@ int main(void) { errno = 0; double pole = logb(0.0); printf("logb(0): %g, %s\n", pole, errno == ERANGE ? "ERANGE" : errno_name(errno)); + /* Volatile, so the compiler folds none of them. */ + volatile double halves[] = { 2.5, -2.5, 0.49999999999999994, 1e19 }; + printf("lround: %ld %ld %ld\n", lround(halves[0]), lround(halves[1]), lround(halves[2])); + errno = 0; + long far = lround(halves[3]); + printf("lround(1e19): %ld, %s\n", far, errno == EDOM ? "EDOM" : errno_name(errno)); + printf("pathconf _PC_PATH_MAX: %ld\n", pathconf(DIR_PATH, _PC_PATH_MAX)); printf("sysconf _SC_PAGE_SIZE: %ld\n", sysconf(_SC_PAGE_SIZE)); return 0; } diff --git a/tests/testcases/tinycc/207_libc_names.expect b/tests/testcases/tinycc/207_libc_names.expect index a5091a68a7f..7723ab5c435 100644 --- a/tests/testcases/tinycc/207_libc_names.expect +++ b/tests/testcases/tinycc/207_libc_names.expect @@ -4,6 +4,14 @@ readlink into 4: 4, "/sys#" readlink into 0: -1, EINVAL readlink of a file: -1, EINVAL readlink of nothing: -1, ENOENT +truncate to 2: 0; stat size: 2 +truncate of nothing: -1, ENOENT +setbuf: file 0 bytes, the buffer holds them +ftello: 10; fseeko 3: 0; ftello: 3; fgetc: 3 +setbuf NULL: file 3 bytes +signal SIGALRM: was SIG_DFL, then SIG_IGN +sigaction SIGALRM: 0 0, SIG_IGN, flags 0x10000000, mask 0x200 +alarm 5: 0; alarm 0: 1 to 5; again: 0 entry: a DT_UNKNOWN entry: b DT_UNKNOWN entry: sub DT_DIR @@ -16,4 +24,7 @@ strsignal: Segmentation fault; Unknown signal modf(-3.25): -3 and -0.25 logb: -4 10 logb(0): -inf, ERANGE +lround: 3 -3 0 +lround(1e19): -9223372036854775808, EDOM +pathconf _PC_PATH_MAX: 4096 sysconf _SC_PAGE_SIZE: 4096 diff --git a/toyos-gpt/src/guid.rs b/toyos-gpt/src/guid.rs index 0564ef80de6..dec1c9895e3 100644 --- a/toyos-gpt/src/guid.rs +++ b/toyos-gpt/src/guid.rs @@ -49,11 +49,6 @@ impl Guid { [0x95, 0x89, 0xA5, 0xC3, 0xCF, 0xD5, 0x55, 0xFD], ); - /// [`Guid::TOYOS_ROOT`]'s canonical text, for a host partition-table writer - /// that names a type by string. `toyos_root_text_is_the_type_guid` is what - /// holds the two spellings together. - pub const TOYOS_ROOT_TEXT: &'static str = "B350BC93-BB6A-4C5E-9589-A5C3CFD555FD"; - /// `064E3777-5076-4C71-8E07-90AD24CFE8D6` — the TOYOS-DATA partition type, /// the writable volume `/apps` and `/home` are two paths into. pub const TOYOS_DATA: Self = Self::from_fields( @@ -63,8 +58,6 @@ impl Guid { [0x8E, 0x07, 0x90, 0xAD, 0x24, 0xCF, 0xE8, 0xD6], ); - pub const TOYOS_DATA_TEXT: &'static str = "064E3777-5076-4C71-8E07-90AD24CFE8D6"; - /// `94464329-E06E-4288-A9DA-7FC7154F5E92` — the slot table's partition /// type: which partitions make each slot, and which slot is marked /// (`toyos_update::slots`). A disk carries one. @@ -75,8 +68,6 @@ impl Guid { [0xA9, 0xDA, 0x7F, 0xC7, 0x15, 0x4F, 0x5E, 0x92], ); - pub const TOYOS_SLOTS_TEXT: &'static str = "94464329-E06E-4288-A9DA-7FC7154F5E92"; - /// `037719D7-DEA5-481A-AA07-6AF8BE6D51E2` — a slot's FAT partition: its /// kernel, its boot parameter and its signed header. A *type*, so it /// selects nothing; the slot table names each one by its unique GUID. @@ -87,8 +78,6 @@ impl Guid { [0xAA, 0x07, 0x6A, 0xF8, 0xBE, 0x6D, 0x51, 0xE2], ); - pub const TOYOS_BOOT_TEXT: &'static str = "037719D7-DEA5-481A-AA07-6AF8BE6D51E2"; - pub const fn from_fields(a: u32, b: u16, c: u16, d: [u8; 8]) -> Self { let a = a.to_le_bytes(); let b = b.to_le_bytes(); @@ -142,21 +131,6 @@ mod tests { assert_eq!(&text[..], b"C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); } - /// The host writes the partition table by naming the type in text and the - /// kernel matches it as bytes; nothing else compares the two spellings. - #[test] - fn toyos_root_text_is_the_type_guid() { - let text = heapless_format(Guid::TOYOS_ROOT); - assert_eq!(&text[..], Guid::TOYOS_ROOT_TEXT.as_bytes()); - let text = heapless_format(Guid::TOYOS_DATA); - assert_eq!(&text[..], Guid::TOYOS_DATA_TEXT.as_bytes()); - let text = heapless_format(Guid::TOYOS_SLOTS); - assert_eq!(&text[..], Guid::TOYOS_SLOTS_TEXT.as_bytes()); - let text = heapless_format(Guid::TOYOS_BOOT); - assert_eq!(&text[..], Guid::TOYOS_BOOT_TEXT.as_bytes()); - assert_ne!(Guid::TOYOS_ROOT, Guid::TOYOS_DATA); - } - #[test] fn zero_is_zero() { assert!(Guid::ZERO.is_zero()); diff --git a/toyos-gpt/src/lib.rs b/toyos-gpt/src/lib.rs index bef84611d09..90ba80cb907 100644 --- a/toyos-gpt/src/lib.rs +++ b/toyos-gpt/src/lib.rs @@ -54,6 +54,11 @@ pub const MAX_ENTRY_ARRAY_BYTES: u64 = 128 * 1024; const MIN_HEADER_BYTES: u32 = 92; /// The smallest a partition entry may be, from the UEFI specification. const MIN_ENTRY_BYTES: u32 = 128; +/// UTF-16 units in an entry's name field, from byte 56 to the end of the +/// 128 bytes every entry has. +const NAME_UNITS: usize = 36; +/// An entry's name field whole, zero units included. +type Name = [u16; NAME_UNITS]; const HEADER_SIGNATURE: &[u8; 8] = b"EFI PART"; const HEADER_REVISION_1_0: u32 = 0x0001_0000; /// The MBR partition type that says "this disk is GPT, keep out". @@ -164,11 +169,12 @@ pub struct Partition { first_lba: u64, last_lba: u64, lba_count: NonZeroU64, + name: Name, } impl Partition { /// `stated`, if its blocks are a partition inside `header`'s usable range. - fn place(stated: Stated, header: &Header) -> Result { + fn place(stated: Stated, name: Name, header: &Header) -> Result { if stated.first < header.first_usable_lba || stated.last > header.last_usable_lba { return Err(stated); } @@ -185,6 +191,7 @@ impl Partition { first_lba: stated.first, last_lba: stated.last, lba_count, + name, }) } @@ -214,6 +221,12 @@ impl Partition { self.lba_count } + /// The entry's name, UTF-16 as the table states it, up to the first zero + /// unit: what it says is the caller's to decode. + pub fn name(&self) -> &[u16] { + self.name.split(|unit| *unit == 0).next().unwrap_or_default() + } + /// Whether this is an ESP *by type*. A sanity check for a log line and /// nothing more — the selection has already happened, by unique GUID. pub fn is_efi_system(&self) -> bool { @@ -455,13 +468,13 @@ fn scan_type_at( let mut slots = out.iter_mut(); // Meaningless unless `walk_entries` returns `Ok`: the array's CRC is // checked at the end of the walk, and an `Err` hands the caller nothing. - let used_entries = walk_entries(dev, &header, disk.lba, &mut |stated| { + let used_entries = walk_entries(dev, &header, disk.lba, &mut |stated, name| { if !keep(stated.type_guid) { return; } matched = matched.saturating_add(1); if let Some(slot) = slots.next() { - *slot = Some(Partition::place(stated, &header)); + *slot = Some(Partition::place(stated, name, &header)); } })?; @@ -483,10 +496,10 @@ fn locate_at( let header = parse_header(block, disk, header_lba)?; let (found, used_entries) = scan_entries(dev, &header, target, disk.lba)?; - let Some(stated) = found else { + let Some((stated, name)) = found else { return Err(GptError::NotFound { used_entries }); }; - let partition = Partition::place(stated, &header) + let partition = Partition::place(stated, name, &header) .map_err(|stated| GptError::PartitionRange { first: stated.first, last: stated.last })?; check_no_overlap(dev, &header, &partition, disk.lba)?; @@ -682,7 +695,7 @@ fn walk_entries( dev: &mut dyn Sectors, header: &Header, lba: LbaSize, - visit: &mut dyn FnMut(Stated), + visit: &mut dyn FnMut(Stated, Name), ) -> Result { let mut block: Block = [0; 4096]; let block = lba.of_block(&mut block); @@ -699,12 +712,12 @@ fn walk_entries( for entry in block.chunks_exact(header.entry_len) { let Some(index) = indices.next() else { break }; crc.update(entry); - let Some(stated) = Stated::decode(index, entry) else { + let (Some(stated), Some(name)) = (Stated::decode(index, entry), name(entry)) else { return Err(GptError::EntrySize(header.entry_bytes)); }; if !stated.type_guid.is_zero() { used = used.saturating_add(1); - visit(stated); + visit(stated, name); } } } @@ -735,17 +748,17 @@ fn scan_entries( header: &Header, target: Guid, lba: LbaSize, -) -> Result<(Option, u32), GptError> { - let mut found: Option = None; +) -> Result<(Option<(Stated, Name)>, u32), GptError> { + let mut found: Option<(Stated, Name)> = None; let mut duplicate: Option<(u32, u32)> = None; - let used = walk_entries(dev, header, lba, &mut |stated| { + let used = walk_entries(dev, header, lba, &mut |stated, name| { if stated.unique_guid != target { return; } match &found { - None => found = Some(stated), - Some(first) if duplicate.is_none() => duplicate = Some((first.index, stated.index)), + None => found = Some((stated, name)), + Some((first, _)) if duplicate.is_none() => duplicate = Some((first.index, stated.index)), Some(_) => {} } })?; @@ -769,7 +782,7 @@ fn check_no_overlap( lba: LbaSize, ) -> Result<(), GptError> { let mut overlap = None; - walk_entries(dev, header, lba, &mut |other| { + walk_entries(dev, header, lba, &mut |other, _| { if overlap.is_none() && other.index != matched.index && other.first <= other.last @@ -790,6 +803,16 @@ fn bytes(buf: &[u8], at: usize) -> Option<[u8; N]> { buf.get(at..)?.first_chunk::().copied() } +/// An entry's name field as UTF-16 units, `None` only where `entry` ends first. +fn name(entry: &[u8]) -> Option { + let field: [u8; 72] = bytes(entry, 56)?; + let mut units = [0; NAME_UNITS]; + for (unit, pair) in units.iter_mut().zip(field.as_chunks::<2>().0) { + *unit = u16::from_le_bytes(*pair); + } + Some(units) +} + fn le_u32(buf: &[u8], at: usize) -> Option { bytes(buf, at).map(u32::from_le_bytes) } diff --git a/toyos-gpt/tests/mutate.rs b/toyos-gpt/tests/mutate.rs index 84d68f18164..e8f5baf106a 100644 --- a/toyos-gpt/tests/mutate.rs +++ b/toyos-gpt/tests/mutate.rs @@ -29,6 +29,7 @@ struct Seen { first: u64, last: u64, count: u64, + name: Vec, } /// An entry the parser handed back as no partition. @@ -59,6 +60,7 @@ fn observe(img: &mut Image, targets: &[Guid]) -> (Result, Answ first: p.first_lba(), last: p.last_lba(), count: p.lba_count().get(), + name: p.name().to_vec(), }; let mut out = [None; 64]; let listed = toyos_gpt::list(img, &mut out).map(|scan| Listed { @@ -78,7 +80,8 @@ fn observe(img: &mut Image, targets: &[Guid]) -> (Result, Answ } fn is(e: &RawEntry, s: &Seen) -> bool { - (e.index, e.type_guid, e.unique, e.first, e.last) == (s.index, s.type_guid, s.unique, s.first, s.last) + let name: Vec = e.name.iter().copied().take_while(|unit| *unit != 0).collect(); + (e.index, e.type_guid, e.unique, e.first, e.last, &name) == (s.index, s.type_guid, s.unique, s.first, s.last, &s.name) } /// A partition's own invariant, against the copy it came from and the device. diff --git a/toyos-gpt/tests/oracle.rs b/toyos-gpt/tests/oracle.rs index 998c115ef6c..26c56f9afcd 100644 --- a/toyos-gpt/tests/oracle.rs +++ b/toyos-gpt/tests/oracle.rs @@ -25,14 +25,15 @@ const VALID: u64 = 2_000; const BENT: u64 = 4_000; const SEED: u64 = 0x6770_745F_6F72_636C; -/// One entry: index, type as text, unique GUID, first and last block. -type Row = (u32, String, Guid, u64, u64); +/// One entry: index, type as text, unique GUID, first and last block, name. +type Row = (u32, String, Guid, u64, u64, String); /// toyos-gpt's reading. struct Ours { disk: Guid, placed: BTreeMap, - unplaced: BTreeMap, + /// Unique GUID, first and last block. + unplaced: BTreeMap, } fn ours(img: &mut Image) -> Result { @@ -43,11 +44,11 @@ fn ours(img: &mut Image) -> Result { for entry in out.iter().flatten() { match entry { Ok(p) => { - let row = (p.index(), p.type_guid().to_string(), p.unique_guid(), p.first_lba(), p.last_lba()); + let row = (p.index(), p.type_guid().to_string(), p.unique_guid(), p.first_lba(), p.last_lba(), String::from_utf16_lossy(p.name())); read.placed.insert(p.index(), row); } Err(u) => { - read.unplaced.insert(u.index, (u.index, u.type_guid.to_string(), u.unique_guid, u.first, u.last)); + read.unplaced.insert(u.index, (u.unique_guid, u.first, u.last)); } } } @@ -80,7 +81,7 @@ fn theirs(img: &Image) -> Theirs { rows: parts .iter() .map(|(&key, p)| { - let row = (key - 1, p.part_type_guid.guid.to_string(), Guid(p.part_guid.to_bytes_le()), p.first_lba, p.last_lba); + let row = (key - 1, p.part_type_guid.guid.to_string(), Guid(p.part_guid.to_bytes_le()), p.first_lba, p.last_lba, p.name.clone()); (key - 1, row) }) .collect(), @@ -128,10 +129,10 @@ fn differ(layout: &Layout, img: &Image, ours: &Result, theirs: & match (o.placed.get(index), o.unplaced.get(index)) { (Some(mine), None) if mine == row => {} // A type only toyos-gpt names: gpt answers the zero GUID. - (Some(mine), None) if row.1 == unnamed && (&mine.0, &mine.2, mine.3, mine.4) == (&row.0, &row.2, row.3, row.4) => { + (Some(mine), None) if row.1 == unnamed && (&mine.0, &mine.2, mine.3, mine.4, &mine.5) == (&row.0, &row.2, row.3, row.4, &row.5) => { named = "a type gpt's own table does not name"; } - (None, Some(stated)) if (stated.2, stated.3, stated.4) == (row.2, row.3, row.4) => { + (None, Some(stated)) if *stated == (row.2, row.3, row.4) => { named = "an entry toyos-gpt places as no partition: gpt checks no range"; } // gpt counts an entry by any non-zero byte, toyos-gpt by its type GUID. diff --git a/toyos-gpt/tests/parse.rs b/toyos-gpt/tests/parse.rs index fbdbc1d9676..9fe8565964a 100644 --- a/toyos-gpt/tests/parse.rs +++ b/toyos-gpt/tests/parse.rs @@ -30,7 +30,7 @@ fn guid(n: u8) -> Guid { } fn entry(index: u32, type_guid: Guid, unique: Guid, first: u64, last: u64) -> RawEntry { - RawEntry { index, type_guid, unique, first, last } + RawEntry { index, type_guid, unique, first, last, name: [0; 36] } } /// The disk every test here breaks one thing of, `edit`ed, with no backup diff --git a/toyos-gpt/tests/table/mod.rs b/toyos-gpt/tests/table/mod.rs index 4d1c0ebd481..252caedf1af 100644 --- a/toyos-gpt/tests/table/mod.rs +++ b/toyos-gpt/tests/table/mod.rs @@ -44,6 +44,15 @@ impl Rng { b[0] |= 1; Guid(b) } + + /// A name field: up to 36 units of any non-zero value, the rest zero. + pub fn name(&mut self) -> [u16; 36] { + let mut name = [0; 36]; + for unit in name.iter_mut().take(self.below(37) as usize) { + *unit = self.range(1, 0xFFFF) as u16; + } + name + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -53,6 +62,8 @@ pub struct RawEntry { pub unique: Guid, pub first: u64, pub last: u64, + /// The name field whole, as UTF-16 units. + pub name: [u16; 36], } /// One copy of the table, every field as the image will state it. @@ -136,7 +147,7 @@ pub fn valid(rng: &mut Rng, shape: &Shape<'_>) -> Layout { break; } floor = last + 1; - entries.push(RawEntry { index, type_guid: rng.pick(shape.types), unique: rng.guid(), first, last }); + entries.push(RawEntry { index, type_guid: rng.pick(shape.types), unique: rng.guid(), first, last, name: rng.name() }); } let primary = Table { @@ -218,7 +229,7 @@ fn mutate_entry(rng: &mut Rng, t: &mut Table, lba_count: u64) { let index = rng.below(u64::from(t.entry_count.clamp(1, 256))) as u32; t.entries.retain(|e| e.index != index); let (first, last) = (edge(rng, t, lba_count), edge(rng, t, lba_count)); - t.entries.push(RawEntry { index, type_guid: Guid::EFI_SYSTEM, unique: rng.guid(), first, last }); + t.entries.push(RawEntry { index, type_guid: Guid::EFI_SYSTEM, unique: rng.guid(), first, last, name: rng.name() }); return; } let k = rng.below(t.entries.len() as u64) as usize; @@ -280,6 +291,14 @@ pub fn image(layout: &Layout) -> Image { slot[16..32].copy_from_slice(&e.unique.0); slot[32..40].copy_from_slice(&e.first.to_le_bytes()); slot[40..48].copy_from_slice(&e.last.to_le_bytes()); + // A nameless entry writes its first 48 bytes alone, so an array laid + // over LBA 0 leaves the MBR's records as they are. + let name = at.and_then(|at| disk.get_mut(at + 56..at + 128)); + if let Some(name) = name.filter(|_| e.name != [0; 36]) { + for (pair, unit) in name.as_chunks_mut::<2>().0.iter_mut().zip(e.name) { + *pair = unit.to_le_bytes(); + } + } } } for &(header_lba, t) in &copies { @@ -393,6 +412,9 @@ impl OnDisk { unique: Guid(e[16..32].try_into().expect("16")), first: u64_at(e, 32), last: u64_at(e, 40), + name: e.get(56..128).map_or([0; 36], |field| { + std::array::from_fn(|i| u16::from_le_bytes([field[2 * i], field[2 * i + 1]])) + }), }) .filter(|e| !e.type_guid.is_zero()) .collect(); diff --git a/userland/compositor/src/main.rs b/userland/compositor/src/main.rs index e0c199c0078..42c1e4f8806 100644 --- a/userland/compositor/src/main.rs +++ b/userland/compositor/src/main.rs @@ -58,10 +58,6 @@ pub const FIXED_POLL_HANDLES: u32 = 3; /// binds. pub const MAX_WINDOW_SLOTS: u32 = Poller::MAX_HANDLES - FIXED_POLL_HANDLES - MAX_PENDING_CONNS; -/// Edge of the cursor sprites, in pixels — the size they are rasterized at and -/// the size the software cursor damages. -pub const CURSOR_PX: u32 = 20; - /// What the launcher offers before `/apps` is read: the label it shows and the /// program it starts. pub const BUILT_IN_APPS: &[(&str, &str)] = diff --git a/userland/compositor/src/session.rs b/userland/compositor/src/session.rs index ba31a622ca8..8cd669da173 100644 --- a/userland/compositor/src/session.rs +++ b/userland/compositor/src/session.rs @@ -10,6 +10,7 @@ use std::process::Command; use std::time::{Duration, Instant}; +use sprite::Sprite; use toyos::endow; use toyos::ipc::RxStep; use toyos::poller::{Poller, READABLE}; @@ -33,7 +34,7 @@ use crate::client::{ use crate::render::{self, Assets, BackBuffer, SystemStats, TitleBarIcons}; use crate::stats::{FrameStats, FrameTotals}; use crate::{ - launcher_apps, CURSOR_PX, DOUBLE_CLICK_TIME, DRAIN_BUDGET, FIXED_POLL_HANDLES, + launcher_apps, DOUBLE_CLICK_TIME, DRAIN_BUDGET, FIXED_POLL_HANDLES, FLAG_HARDWARE_CURSOR, FRAME_INTERVAL, MAX_WINDOW_SLOTS, STATS_INTERVAL, }; @@ -43,19 +44,27 @@ const _: () = assert!( ); struct Cursors { - default: sprite::Sprite, - crosshair: sprite::Sprite, - resize: sprite::Sprite, + default: Sprite, + crosshair: Sprite, + resize: Sprite, } impl Cursors { - fn get(&self, style: CursorStyle) -> &sprite::Sprite { + fn get(&self, style: CursorStyle) -> &Sprite { match style { CursorStyle::Crosshair => &self.crosshair, CursorStyle::Resize => &self.resize, CursorStyle::Default => &self.default, } } + + /// What the software cursor damages, whichever cursor it moves from and to. + fn extent(&self) -> (i32, i32) { + let all = [&self.default, &self.crosshair, &self.resize]; + let w = all.iter().map(|s| s.width()).max().unwrap(); + let h = all.iter().map(|s| s.height()).max().unwrap(); + (w as i32, h as i32) + } } /// The wallpaper as it is stored: width, height, then RGB triples. @@ -174,13 +183,9 @@ impl Session { let mut cursor_shm = SharedMemory::adopt(fb_info.cursor, 64 * 64 * 4) .expect("the cursor buffer the framebuffer claim just handed over"); let cursors = Cursors { - default: read_sprite("/system/share/icons/cursor-bold.svg", CURSOR_PX, [255, 255, 255]), - resize: read_sprite( - "/system/share/icons/arrow-down-right-bold.svg", - CURSOR_PX, - [255, 255, 255], - ), - crosshair: read_sprite("/system/share/icons/crosshair-simple-bold.svg", CURSOR_PX, [0, 0, 0]), + default: Sprite::icon("cursor-bold", [255, 255, 255]), + resize: Sprite::icon("arrow-down-right-bold", [255, 255, 255]), + crosshair: Sprite::icon("crosshair-simple-bold", [0, 0, 0]), }; render::upload_cursor(&fb_dev, cursor_shm.as_mut_slice(), &cursors.default, hw_cursor); @@ -205,9 +210,9 @@ impl Session { wallpaper.rescale(&screen); let icons = TitleBarIcons { - minimize: read_sprite("/system/share/icons/minus-bold.svg", 14, [255, 255, 255]), - maximize: read_sprite("/system/share/icons/square-bold.svg", 14, [255, 255, 255]), - close: read_sprite("/system/share/icons/x-bold.svg", 14, [255, 255, 255]), + minimize: Sprite::icon("minus-bold", [255, 255, 255]), + maximize: Sprite::icon("square-bold", [255, 255, 255]), + close: Sprite::icon("x-bold", [255, 255, 255]), }; let apps = launcher_apps(); @@ -450,9 +455,9 @@ impl Session { self.fb_dev.move_cursor(self.cursor.x as u32, self.cursor.y as u32) .expect("compositor holds the framebuffer claim"); } else { - let px = CURSOR_PX as i32; - self.damage.add(Rect::new(was.x, was.y, px, px)); - self.damage.add(Rect::new(self.cursor.x, self.cursor.y, px, px)); + let (w, h) = self.cursors.extent(); + self.damage.add(Rect::new(was.x, was.y, w, h)); + self.damage.add(Rect::new(self.cursor.x, self.cursor.y, w, h)); } let delta = Point { x: self.cursor.x - was.x, y: self.cursor.y - was.y }; @@ -1388,11 +1393,6 @@ fn desk_of(screen: &Screen, font: &font::Font, apps: usize) -> Desk { } } -fn read_sprite(path: &str, size: u32, color: [u8; 3]) -> sprite::Sprite { - let svg = std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {path}: {e}")); - sprite::Sprite::from_svg_colored(&svg, size, color) -} - /// Total physical memory, as the kernel reports it. fn total_memory() -> u64 { let mut buf = [0u8; system::SYSINFO_HEADER_SIZE]; diff --git a/userland/files/src/main.rs b/userland/files/src/main.rs index 792a64da88c..de74fbe9c5f 100644 --- a/userland/files/src/main.rs +++ b/userland/files/src/main.rs @@ -13,7 +13,6 @@ const PATH_COLOR: Color = Color { r: 0x80, g: 0x80, b: 0x90 }; const SCROLLBAR_TRACK: Color = Color { r: 0x25, g: 0x25, b: 0x35 }; const SCROLLBAR_THUMB: Color = Color { r: 0x55, g: 0x55, b: 0x65 }; -const ICON_SIZE: usize = 32; const ITEM_WIDTH: usize = 80; const ITEM_HEIGHT: usize = 64; const PADDING: usize = 8; @@ -60,10 +59,8 @@ impl FileBrowser { let font_data = fs::read("/system/share/fonts/JetBrainsMono-Regular-8x16.font").expect("failed to read font"); let font = font::Font::from_prebuilt(&font_data); - let folder_svg = fs::read("/system/share/icons/folder-bold.svg").expect("failed to read folder icon"); - let file_svg = fs::read("/system/share/icons/file-bold.svg").expect("failed to read file icon"); - let folder_icon = Sprite::from_svg_colored(&folder_svg, ICON_SIZE as u32, [0xf0, 0xc8, 0x50]); - let file_icon = Sprite::from_svg_colored(&file_svg, ICON_SIZE as u32, [0xd0, 0xd0, 0xd8]); + let folder_icon = Sprite::icon("folder-bold", [0xf0, 0xc8, 0x50]); + let file_icon = Sprite::icon("file-bold", [0xd0, 0xd0, 0xd8]); let current_dir = std::env::home_dir().expect("files: the supervisor starts every program with HOME"); @@ -167,13 +164,9 @@ impl FileBrowser { self.fb.fill_rect(x, clip_y, ITEM_WIDTH, clip_h, SELECTED_BG); } - if y >= content_top && y + ICON_SIZE <= content_bottom { - let icon_x = x + (ITEM_WIDTH - ICON_SIZE) / 2; - let icon = if entry.is_dir { - &self.folder_icon - } else { - &self.file_icon - }; + let icon = if entry.is_dir { &self.folder_icon } else { &self.file_icon }; + if y >= content_top && y + icon.height() <= content_bottom { + let icon_x = x + (ITEM_WIDTH - icon.width()) / 2; icon.draw( self.fb.ptr(), self.fb.stride(), @@ -190,7 +183,7 @@ impl FileBrowser { let max_chars = ITEM_WIDTH / self.font.width(); let name: String = entry.name.chars().take(max_chars).collect(); let text_x = x + (ITEM_WIDTH.saturating_sub(name.len() * self.font.width())) / 2; - let text_y = y + ICON_SIZE + 2; + let text_y = y + icon.height() + 2; if text_y >= content_top && text_y + self.font.height() <= content_bottom { self.font.draw_string(&self.fb, text_x, text_y, &name, TEXT_COLOR, text_bg); } diff --git a/userland/libc/include/math.h b/userland/libc/include/math.h index a8b572551a6..3ca45b1e729 100644 --- a/userland/libc/include/math.h +++ b/userland/libc/include/math.h @@ -61,6 +61,7 @@ double sinh(double x); double cosh(double x); double tanh(double x); double round(double x); +long lround(double x); double trunc(double x); double modf(double x, double *iptr); double logb(double x); diff --git a/userland/libc/include/stdio.h b/userland/libc/include/stdio.h index 27f39a1c48f..900f7424aa8 100644 --- a/userland/libc/include/stdio.h +++ b/userland/libc/include/stdio.h @@ -3,6 +3,7 @@ #include #include +#include #include #ifdef __cplusplus @@ -40,6 +41,9 @@ size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); int fseek(FILE *stream, long offset, int whence); long ftell(FILE *stream); +int fseeko(FILE *stream, off_t offset, int whence); +off_t ftello(FILE *stream); +void setbuf(FILE *stream, char *buf); void rewind(FILE *stream); int fflush(FILE *stream); int feof(FILE *stream); diff --git a/userland/libc/include/sys/time.h b/userland/libc/include/sys/time.h index 2dec877691f..2c9f97ea789 100644 --- a/userland/libc/include/sys/time.h +++ b/userland/libc/include/sys/time.h @@ -18,6 +18,7 @@ struct timezone { }; int gettimeofday(struct timeval *tv, struct timezone *tz); +int utimes(const char *path, const struct timeval times[2]); #ifdef __cplusplus } diff --git a/userland/libc/include/sys/wait.h b/userland/libc/include/sys/wait.h index f60449eb1c3..f87db19b441 100644 --- a/userland/libc/include/sys/wait.h +++ b/userland/libc/include/sys/wait.h @@ -15,7 +15,11 @@ extern "C" { #define WIFEXITED(s) (WTERMSIG(s) == 0) #define WIFSIGNALED(s) (WTERMSIG(s) != 0) +struct rusage; + pid_t waitpid(pid_t pid, int *status, int options); +pid_t wait(int *status); +pid_t wait4(pid_t pid, int *status, int options, struct rusage *usage); #ifdef __cplusplus } diff --git a/userland/libc/include/unistd.h b/userland/libc/include/unistd.h index c93aa56e297..6024325d058 100644 --- a/userland/libc/include/unistd.h +++ b/userland/libc/include/unistd.h @@ -24,6 +24,7 @@ char *getcwd(char *buf, size_t size); int chdir(const char *path); int access(const char *path, int mode); unsigned int sleep(unsigned int seconds); +unsigned int alarm(unsigned int seconds); int usleep(unsigned int usec); int isatty(int fd); int execvp(const char *file, char *const argv[]); @@ -34,6 +35,8 @@ int pipe(int pipefd[2]); void _exit(int status); int fsync(int fd); int ftruncate(int fd, off_t length); +int truncate(const char *path, off_t length); +long pathconf(const char *path, int name); ssize_t readlink(const char *path, char *buf, size_t size); int symlink(const char *target, const char *linkpath); int link(const char *existing, const char *newpath); @@ -59,6 +62,8 @@ long sysconf(int name); #define _SC_CLK_TCK 2 #define _SC_NPROCESSORS_ONLN 84 +#define _PC_PATH_MAX 4 + #define F_OK 0 #define R_OK 4 #define W_OK 2 diff --git a/userland/libc/src/alarm.rs b/userland/libc/src/alarm.rs new file mode 100644 index 00000000000..391af7d15b5 --- /dev/null +++ b/userland/libc/src/alarm.rs @@ -0,0 +1,94 @@ +//! `alarm`: one per process, kept by a thread of this library's, started by +//! the first alarm armed, that sleeps until it is due. Then, with `SIGALRM` +//! ignored, the alarm is gone; otherwise the keeper does what `SIGALRM`'s +//! default action does and ends the process. A handler `signal` or `sigaction` +//! is given never runs, and a mask that blocks `SIGALRM` holds nothing back +//! (`issues/an-alarm-reaches-no-sigalrm-handler.md`). + +use core::ptr; +use core::sync::atomic::{AtomicU32, Ordering}; + +use toyos_abi::syscall; + +use crate::alarmreq; +use crate::misc::SigAction; +use crate::pthread::Lock; + +/// What the process ends with when its alarm is due: the code of an end by +/// `SIGALRM`, as `abort`'s is `SIGABRT`'s. +const ALARM_END: i32 = 128 + alarmreq::SIGALRM; + +struct Alarm { + /// When the alarm is due, in the monotonic clock's nanoseconds. + due: Option, + /// Whether the keeper runs. + kept: bool, +} + +static ALARM: Lock = Lock::new(Alarm { due: None, kept: false }); + +/// Moved at every `alarm`, so a keeper asleep on an older one wakes. +static TURN: AtomicU32 = AtomicU32::new(0); + +/// `SIGALRM`'s action, whose handler is its disposition: `SIG_DFL`, `SIG_IGN` +/// or a handler's address. +static ACTION: Lock = Lock::new(SigAction::of(alarmreq::SIG_DFL)); + +/// Make `new`, if any, `SIGALRM`'s action, and answer the one it replaces whole. +pub(crate) fn act(new: Option) -> SigAction { + let mut action = ACTION.lock(); + let old = *action; + if let Some(new) = new { + *action = new; + } + old +} + +fn now() -> u64 { + toyos_abi::clock::nanos_since_boot() +} + +#[no_mangle] +pub extern "C" fn alarm(seconds: u32) -> u32 { + let now = now(); + let mut alarm = ALARM.lock(); + let left = alarmreq::left(alarm.due, now); + alarm.due = alarmreq::due(now, seconds); + if alarm.due.is_some() && !alarm.kept { + // Never joined: it lives as long as the process. + let mut thread = 0u64; + // SAFETY: `keep` is a thread entry that takes and returns nothing. + let refused = unsafe { crate::pthread::pthread_create(&mut thread, ptr::null(), keep, ptr::null_mut()) }; + assert_eq!(refused, 0, "alarm: no thread to keep it"); + alarm.kept = true; + } + TURN.fetch_add(1, Ordering::Release); + // SAFETY: `TURN` is a live, aligned u32. + unsafe { syscall::futex_wake(TURN.as_ptr(), 1) }; + left +} + +/// Sleep until the alarm is due or moved, and once it is due, drop it or end +/// the process as [`alarmreq::ends`] says. +unsafe extern "C" fn keep(_: *mut u8) -> *mut u8 { + loop { + // Read before the alarm, so an `alarm` between the two moves it and + // the wait below returns at once. + let turn = TURN.load(Ordering::Acquire); + let mut alarm = ALARM.lock(); + let wait = match alarm.due { + None => None, + Some(due) => match due.checked_sub(now()) { + Some(wait) if wait > 0 => Some(wait), + _ if alarmreq::ends(act(None).handler) => syscall::exit(ALARM_END), + _ => { + alarm.due = None; + None + } + }, + }; + drop(alarm); + // SAFETY: `TURN` is a live, aligned u32. + unsafe { syscall::futex_wait(TURN.as_ptr(), turn, wait) }; + } +} diff --git a/userland/libc/src/alarmreq.rs b/userland/libc/src/alarmreq.rs new file mode 100644 index 00000000000..e8c7f2c1d7f --- /dev/null +++ b/userland/libc/src/alarmreq.rs @@ -0,0 +1,33 @@ +//! What `alarm` answers and arms, in nanoseconds of the monotonic clock, and +//! what a due one does. It reads nothing but what it is handed, so the host +//! tests it (`toyos-libc-copies`). + +const NANOS_PER_SEC: u64 = 1_000_000_000; + +/// `signal.h`'s `SIGALRM`, and its `SIG_DFL` and `SIG_IGN` as addresses. +pub(crate) const SIGALRM: i32 = 14; +pub(crate) const SIG_DFL: usize = 0; +pub(crate) const SIG_IGN: usize = 1; + +/// Whether an alarm due under `SIGALRM`'s disposition `handler` ends the +/// process: it does unless `SIGALRM` is ignored, since no handler runs +/// (`issues/an-alarm-reaches-no-sigalrm-handler.md`). +pub(crate) fn ends(handler: usize) -> bool { + handler != SIG_IGN +} + +/// When an alarm of `seconds` armed at `now` is due; none for 0, which +/// disarms. +pub(crate) fn due(now: u64, seconds: u32) -> Option { + // No overflow: `u32::MAX` seconds are under 2^62 nanoseconds, and `now` + // counts from boot. + (seconds != 0).then(|| now + u64::from(seconds) * NANOS_PER_SEC) +} + +/// The seconds an alarm due at `due` has left at `now`, rounded up: one that +/// has any time left answers at least 1, and none, or one already due, 0. +pub(crate) fn left(due: Option, now: u64) -> u32 { + let Some(due) = due.filter(|&due| due > now) else { return 0 }; + // At most the `u32` seconds `due` was armed with. + (due - now).div_ceil(NANOS_PER_SEC) as u32 +} diff --git a/userland/libc/src/errno.rs b/userland/libc/src/errno.rs index 6bd30ef43b2..a6bd0f10ba6 100644 --- a/userland/libc/src/errno.rs +++ b/userland/libc/src/errno.rs @@ -33,6 +33,7 @@ pub(crate) const ENODEV: i32 = 19; pub(crate) const EINVAL: i32 = 22; pub(crate) const ENOSPC: i32 = 28; pub(crate) const EPIPE: i32 = 32; +pub(crate) const EDOM: i32 = 33; pub(crate) const ERANGE: i32 = 34; pub(crate) const EDEADLK: i32 = 35; pub(crate) const ENOSYS: i32 = 38; diff --git a/userland/libc/src/fparts.rs b/userland/libc/src/fparts.rs index ff89184e8eb..2531f2ae82e 100644 --- a/userland/libc/src/fparts.rs +++ b/userland/libc/src/fparts.rs @@ -1,5 +1,6 @@ -//! A double's parts: `modf`'s integral and fractional parts and `logb`'s -//! exponent, read off its bits. It reads and sets nothing but what it is +//! A double's parts: `modf`'s integral and fractional parts, the integer +//! `round` and `lround` take from them, and `logb`'s exponent, read off its +//! bits. It reads and sets nothing but what it is //! handed, so the host holds it to its own C library's (`toyos-libc-copies`). const MANTISSA_BITS: u32 = 52; @@ -37,6 +38,31 @@ pub(crate) fn modf(x: f64) -> (f64, f64) { (int, x - int) } +/// `round`: `x` to the nearest integer, a half away from zero, keeping its +/// sign; an integer, an infinity or a NaN is itself. +pub(crate) fn round(x: f64) -> f64 { + let (int, fraction) = modf(x); + if fraction.abs() < 0.5 { + return int; + } + // Exact: below 2^52 every integer and its successor are doubles, and at or + // above it `modf` answers no fraction. + if x.is_sign_negative() { int - 1.0 } else { int + 1.0 } +} + +/// `lround` of a value whose rounding is no `long`, a NaN among them: POSIX's +/// domain error, `errno` `EDOM`. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct Domain; + +/// `lround`: [`round`] as a `long`. +pub(crate) fn lround(x: f64) -> Result { + // `long`'s range as doubles, `[-2^63, 2^63)`; a NaN is in neither half. + const LIMIT: f64 = 9_223_372_036_854_775_808.0; + let rounded = round(x); + if (-LIMIT..LIMIT).contains(&rounded) { Ok(rounded as i64) } else { Err(Domain) } +} + /// `logb` at a zero: POSIX's pole error, `-inf` with `errno` `ERANGE`. #[derive(Debug, PartialEq, Eq)] pub(crate) struct Pole; diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index ef7401847d1..1cd62e646e5 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -4,6 +4,8 @@ extern crate alloc; +mod alarm; +mod alarmreq; mod arch; mod ctype; mod elfsym; diff --git a/userland/libc/src/math.rs b/userland/libc/src/math.rs index 3176755636b..434535b97b7 100644 --- a/userland/libc/src/math.rs +++ b/userland/libc/src/math.rs @@ -210,7 +210,16 @@ pub extern "C" fn trunc(x: f64) -> f64 { #[no_mangle] pub extern "C" fn round(x: f64) -> f64 { - floor(x + 0.5) + crate::fparts::round(x) +} + +/// A domain error answers `long`'s least value, as x86-64's conversion does. +#[no_mangle] +pub extern "C" fn lround(x: f64) -> i64 { + crate::fparts::lround(x).unwrap_or_else(|crate::fparts::Domain| { + crate::errno::set(crate::errno::EDOM); + i64::MIN + }) } #[no_mangle] diff --git a/userland/libc/src/misc.rs b/userland/libc/src/misc.rs index ba2b635eeb6..e707e1478a4 100644 --- a/userland/libc/src/misc.rs +++ b/userland/libc/src/misc.rs @@ -6,6 +6,7 @@ use core::sync::atomic::{AtomicU32, Ordering}; use toyos_abi::syscall; +use crate::alarmreq::SIGALRM; use crate::errno::{ECHILD, ENOSYS}; use crate::strtonum; @@ -122,6 +123,18 @@ pub unsafe extern "C" fn waitpid(_pid: i32, _status: *mut i32, _options: i32) -> -1 } +/// `waitpid(-1, status, 0)`. +#[no_mangle] +pub unsafe extern "C" fn wait(status: *mut i32) -> i32 { + unsafe { waitpid(-1, status, 0) } +} + +/// [`waitpid`]: it answers no child, so no usage is written. +#[no_mangle] +pub unsafe extern "C" fn wait4(pid: i32, status: *mut i32, options: i32, _usage: *mut u8) -> i32 { + unsafe { waitpid(pid, status, options) } +} + // Exit / abort / atexit /// A handler `exit` runs: `atexit`'s takes nothing, `__cxa_atexit`'s its object. @@ -195,20 +208,45 @@ pub unsafe extern "C" fn abort() -> ! { syscall::exit(134) // SIGABRT } -// Signal (stubs — ToyOS has no signals) +// Signal (stubs — ToyOS has no signals, but `SIGALRM`'s disposition decides +// what a due `alarm` does) -type SigHandlerT = unsafe extern "C" fn(i32); +/// `signal.h`'s `struct sigaction`. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SigAction { + pub(crate) handler: usize, + flags: u64, + restorer: usize, + mask: u64, +} + +impl SigAction { + /// The action of `handler` alone, as `signal` makes one. + pub(crate) const fn of(handler: usize) -> Self { + Self { handler, flags: 0, restorer: 0, mask: 0 } + } +} +/// A handler by its address: `SIG_DFL` and `SIG_IGN` are no functions. #[no_mangle] -pub unsafe extern "C" fn signal(_signum: i32, handler: SigHandlerT) -> SigHandlerT { - handler // return the handler as "previous", effectively a no-op +pub unsafe extern "C" fn signal(signum: i32, handler: *const u8) -> *const u8 { + match signum { + SIGALRM => crate::alarm::act(Some(SigAction::of(handler as usize))).handler as *const u8, + _ => handler, // return the handler as "previous", effectively a no-op + } } #[no_mangle] -pub unsafe extern "C" fn sigaction( - _signum: i32, _act: *const u8, _oldact: *mut u8, -) -> i32 { - 0 // success +pub unsafe extern "C" fn sigaction(signum: i32, act: *const SigAction, oldact: *mut SigAction) -> i32 { + if signum != SIGALRM { + return 0; // success + } + let old = crate::alarm::act(unsafe { act.as_ref() }.copied()); + if let Some(oldact) = unsafe { oldact.as_mut() } { + *oldact = old; + } + 0 } /// The calling thread's mask, which is what POSIX's process mask is in a diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index 47dfd48e70f..154ce1ca7f5 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -178,6 +178,34 @@ pub unsafe extern "C" fn ftruncate(raw_fd: i32, length: i64) -> i32 { } } +/// [`ftruncate`] of `path` opened for writing. +#[no_mangle] +pub unsafe extern "C" fn truncate(path: *const u8, length: i64) -> i32 { + let fd = unsafe { open(path, O_WRONLY, 0) }; + if fd < 0 { + return -1; + } + let truncated = unsafe { ftruncate(fd, length) }; + unsafe { close(fd) }; + truncated +} + +/// `_PC_PATH_MAX`, `unistd.h`'s. +const PC_PATH_MAX: i32 = 4; + +/// The longest path the kernel resolves, for every path; no other limit is +/// answered. +#[no_mangle] +pub unsafe extern "C" fn pathconf(_path: *const u8, name: i32) -> i64 { + match name { + PC_PATH_MAX => toyos::fs::MAX_PATH as i64, + _ => { + crate::errno::set(EINVAL); + -1 + } + } +} + #[no_mangle] pub unsafe extern "C" fn fsync(raw_fd: i32) -> i32 { match syscall::fsync(fd(raw_fd)) { diff --git a/userland/libc/src/refused.rs b/userland/libc/src/refused.rs index b6f1531c8a9..972236f0198 100644 --- a/userland/libc/src/refused.rs +++ b/userland/libc/src/refused.rs @@ -71,6 +71,12 @@ pub extern "C" fn fchmod(_fd: i32, _mode: u32) -> i32 { refuse() } +/// No call sets a file's times. +#[no_mangle] +pub unsafe extern "C" fn utimes(_path: *const u8, _times: *const u8) -> i32 { + refuse() +} + /// No call answers a filesystem's size or free space. #[no_mangle] pub unsafe extern "C" fn statvfs(_path: *const u8, _buf: *mut u8) -> i32 { diff --git a/userland/libc/src/stdio.rs b/userland/libc/src/stdio.rs index 00e558458a8..c9d6fcf62f0 100644 --- a/userland/libc/src/stdio.rs +++ b/userland/libc/src/stdio.rs @@ -458,6 +458,35 @@ pub unsafe extern "C" fn ftell(f: *mut FILE) -> i64 { if at < 0 { at } else { at - unsafe { (*f).unget_len } as i64 } } +/// [`fseek`]: an `off_t` is a `long`. +#[no_mangle] +pub unsafe extern "C" fn fseeko(f: *mut FILE, offset: i64, whence: i32) -> i32 { + unsafe { fseek(f, offset, whence) } +} + +/// [`ftell`]: an `off_t` is a `long`. +#[no_mangle] +pub unsafe extern "C" fn ftello(f: *mut FILE) -> i64 { + unsafe { ftell(f) } +} + +/// `buf`, `BUFSIZ` bytes its caller keeps, as `f`'s buffer, fully buffered; a +/// null `buf` leaves `f` unbuffered. What `f` held pending is written first, so +/// the buffer it replaces is free to go. +#[no_mangle] +pub unsafe extern "C" fn setbuf(f: *mut FILE, buf: *mut u8) { + unsafe { + flush_buf(f); + if (*f).owned { + super::memory::free((*f).buf); + } + (*f).buf = buf; + (*f).owned = false; + (*f).cap = if buf.is_null() { 0 } else { BUFSIZ }; + (*f).mode = if buf.is_null() { IONBF } else { IOFBF }; + } +} + #[no_mangle] pub unsafe extern "C" fn rewind(f: *mut FILE) { if !f.is_null() { diff --git a/userland/soundserver/Cargo.toml b/userland/soundserver/Cargo.toml index 4926eac28ca..59735886004 100644 --- a/userland/soundserver/Cargo.toml +++ b/userland/soundserver/Cargo.toml @@ -13,7 +13,7 @@ toyos-virtio-sound = { path = "virtio-sound" } toyos-virtio = { path = "../../toyos-virtio" } toyos-pci-claim = { path = "../toyos-pci-claim" } toyos-inspect = { path = "../../toyos-inspect" } -rubato = "0.16" +rubato = { version = "0.16", default-features = false } [package.metadata.toyos.host] exempt.owns = "ToyOS's audio devices, claimed from the kernel" diff --git a/userland/sprite/Cargo.toml b/userland/sprite/Cargo.toml index 0856af22ce9..86090427671 100644 --- a/userland/sprite/Cargo.toml +++ b/userland/sprite/Cargo.toml @@ -1,12 +1,9 @@ [package] name = "sprite" -description = "An SVG rasterized through resvg into an RGBA pixel buffer, optionally in one colour." +description = "An icon the build drew, in one colour, as an RGBA pixel buffer, and its alpha-blended blit." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" [lib] doctest = false - -[dependencies] -resvg = { version = "0.47", default-features = false } diff --git a/userland/sprite/README.md b/userland/sprite/README.md deleted file mode 100644 index 6ee2855e057..00000000000 --- a/userland/sprite/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# sprite - -SVG rasterizer that converts vector graphics to RGBA pixel buffers, with support for color substitution. - -Used by compositor and files. diff --git a/userland/sprite/src/lib.rs b/userland/sprite/src/lib.rs index b1fd10922f0..d2096b69f65 100644 --- a/userland/sprite/src/lib.rs +++ b/userland/sprite/src/lib.rs @@ -1,5 +1,4 @@ -use resvg::tiny_skia; -use resvg::usvg; +//! An icon the build drew (`src/icons.rs`), in one colour, and its blit. pub struct Sprite { width: usize, @@ -8,34 +7,16 @@ pub struct Sprite { } impl Sprite { - /// Rasterize an SVG at the given pixel size. The SVG is scaled to fit a `size x size` square. - pub fn from_svg(svg_bytes: &[u8], size: u32) -> Self { - let tree = usvg::Tree::from_data(svg_bytes, &usvg::Options::default()) - .expect("failed to parse SVG"); - let mut pixmap = tiny_skia::Pixmap::new(size, size).expect("failed to create pixmap"); - let svg_size = tree.size(); - let sx = size as f32 / svg_size.width(); - let sy = size as f32 / svg_size.height(); - resvg::render(&tree, tiny_skia::Transform::from_scale(sx, sy), &mut pixmap.as_mut()); - // resvg outputs premultiplied RGBA; convert to straight alpha - let mut data = pixmap.take(); - for chunk in data.chunks_exact_mut(4) { - let a = chunk[3] as u16; - if a > 0 && a < 255 { - chunk[0] = ((chunk[0] as u16 * 255) / a) as u8; - chunk[1] = ((chunk[1] as u16 * 255) / a) as u8; - chunk[2] = ((chunk[2] as u16 * 255) / a) as u8; - } - } - Self { width: size as usize, height: size as usize, data } - } - - /// Rasterize an SVG, replacing `currentColor` with the given RGB color. - pub fn from_svg_colored(svg_bytes: &[u8], size: u32, color: [u8; 3]) -> Self { - let svg_str = String::from_utf8_lossy(svg_bytes); - let hex = format!("#{:02x}{:02x}{:02x}", color[0], color[1], color[2]); - let replaced = svg_str.replace("currentColor", &hex); - Self::from_svg(replaced.as_bytes(), size) + /// The icon `/system/share/icons/.alpha`, in `color`. + pub fn icon(stem: &str, color: [u8; 3]) -> Self { + let path = format!("/system/share/icons/{stem}.alpha"); + let file = std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}")); + let (size, mask) = file.split_at_checked(8).unwrap_or_else(|| panic!("{path} has no header")); + let width = u32::from_le_bytes(size[..4].try_into().unwrap()) as usize; + let height = u32::from_le_bytes(size[4..].try_into().unwrap()) as usize; + assert_eq!(mask.len(), width * height, "{path} is not {width}x{height}"); + let data = mask.iter().flat_map(|&a| [color[0], color[1], color[2], a]).collect(); + Self { width, height, data } } pub fn width(&self) -> usize {