feat(host,api): the host knows what OS it runs, and says so
Clients want to show which OS/distro a host runs. The host now detects it once per process (new osinfo module: compile-time on Windows/macOS, os-release on Linux) and advertises an icon-friendly specificity chain, generic → specific — `windows`, `macos`, `linux[/<family>][/<id>]`, e.g. `linux/fedora/bazzite`, `linux/arch/steamos`. A client walks the chain most-specific-first and shows the first token it has art for, so an unknown distro degrades to its family's mark and finally to plain Tux — the host emits the full chain precisely so clients need zero distro→parent knowledge. The middle token is the first recognized ID_LIKE ancestor (the spec orders them most-similar-first); the leaf is ID verbatim, sanitized to TXT-safe `[a-z0-9._-]` because it feeds a DNS record. Two carriers, both additive: a new advisory mDNS `os=` TXT key (same trust posture as `mac` — unauthenticated, a wrong value only draws a wrong icon), and `HostInfo.os` + `HostInfo.os_name` on the mgmt API (`os_name` is the os-release PRETTY_NAME, REST-only so the TXT stays small). GameStream serverinfo and the QUIC Welcome are untouched. api/openapi.json regenerated (drift test green on Linux) and the TS SDK gen refreshed from it — which also catches the committed gen up with earlier spec changes it had missed (monitors, native_paired_clients, encoder_backend). assets/os-icons/ gains the ten master SVG marks every client derives its per-platform art from (Font Awesome Free brands CC BY 4.0 + Simple Icons CC0 — provenance in its README), with attribution folded into THIRD-PARTY-NOTICES.txt via the generator (regenerating also catches the stale crate manifest up with the current lockfile). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -5335,6 +5335,8 @@
|
||||
"abi_version",
|
||||
"app_version",
|
||||
"gfe_version",
|
||||
"os",
|
||||
"os_name",
|
||||
"codecs",
|
||||
"gamestream",
|
||||
"ports"
|
||||
@@ -5372,6 +5374,16 @@
|
||||
"type": "string",
|
||||
"description": "Best-effort primary LAN IP."
|
||||
},
|
||||
"os": {
|
||||
"type": "string",
|
||||
"description": "OS identity chain, generic → most specific, slash-separated (`windows` | `macos` |\n`linux[/<family>][/<id>]`). A client walks it most-specific-first and shows the first\ntoken it has an icon for, so an unknown distro still degrades to its family's mark.",
|
||||
"example": "linux/fedora/bazzite"
|
||||
},
|
||||
"os_name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable OS name (os-release `PRETTY_NAME`; `\"Windows\"`/`\"macOS\"` elsewhere).",
|
||||
"example": "Bazzite 42 (Kinoite)"
|
||||
},
|
||||
"ports": {
|
||||
"$ref": "#/components/schemas/PortMap"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
Font Awesome Free — brand icons (windows, apple, linux, steam, ubuntu, fedora,
|
||||
opensuse in assets/os-icons/) are from Font Awesome Free.
|
||||
|
||||
Copyright (c) Fonticons, Inc. (https://fontawesome.com)
|
||||
|
||||
Font Awesome Free icons are licensed under the Creative Commons Attribution 4.0
|
||||
International license (CC BY 4.0), https://creativecommons.org/licenses/by/4.0/.
|
||||
The icons are redistributed here as monochrome SVG path data with no
|
||||
modifications beyond color normalization (fill="currentColor").
|
||||
|
||||
Per the Font Awesome Free license (https://fontawesome.com/license/free):
|
||||
"Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Attribution is required by MIT, SIL OFL, and CC BY licenses."
|
||||
|
||||
Brand icons are trademarks of their respective owners and are used for
|
||||
identification purposes only; their use does not imply endorsement.
|
||||
@@ -0,0 +1,10 @@
|
||||
Simple Icons — brand icons (arch, nixos, debian in assets/os-icons/) are from
|
||||
Simple Icons (https://simpleicons.org, https://github.com/simple-icons/simple-icons).
|
||||
|
||||
The Simple Icons SVG path data is released under CC0 1.0 Universal (public domain
|
||||
dedication), https://creativecommons.org/publicdomain/zero/1.0/ — no attribution
|
||||
required; this notice is provided for provenance.
|
||||
|
||||
Brand icons are trademarks of their respective owners and are used for
|
||||
identification purposes only; their use does not imply endorsement. See
|
||||
https://github.com/simple-icons/simple-icons/blob/develop/DISCLAIMER.md.
|
||||
@@ -0,0 +1,30 @@
|
||||
# OS icon masters
|
||||
|
||||
The canonical OS/distro brand marks every client derives its host-card OS icon from
|
||||
(web console inline SVGs, GTK symbolic icons, Windows PNGs, Apple template imagesets,
|
||||
Android `ImageVector`s). One file per **icon token** of the host's OS-identity chain
|
||||
(see `crates/punktfunk-host/src/osinfo.rs` and `crates/pf-client-core/src/os.rs`):
|
||||
|
||||
| token | mark | source |
|
||||
|---|---|---|
|
||||
| `windows` | Windows | Font Awesome Free brands (CC BY 4.0) |
|
||||
| `apple` | Apple (also `macos` via alias) | Font Awesome Free brands (CC BY 4.0) |
|
||||
| `linux` | Tux | Font Awesome Free brands (CC BY 4.0) |
|
||||
| `steam` | Steam (also `steamos` via alias) | Font Awesome Free brands (CC BY 4.0) |
|
||||
| `ubuntu` | Ubuntu | Font Awesome Free brands (CC BY 4.0) |
|
||||
| `fedora` | Fedora | Font Awesome Free brands (CC BY 4.0) |
|
||||
| `opensuse` | SUSE | Font Awesome Free brands (CC BY 4.0) |
|
||||
| `arch` | Arch Linux | Simple Icons (CC0 1.0) |
|
||||
| `nixos` | NixOS | Simple Icons (CC0 1.0) |
|
||||
| `debian` | Debian | Simple Icons (CC0 1.0) |
|
||||
|
||||
Distros with no file here (Bazzite, CachyOS, Nobara, Pop!_OS, Mint, …) are intentional:
|
||||
the host advertises the full chain (`linux/fedora/bazzite`), and clients walk it
|
||||
most-specific-first, so they degrade to the family mark and finally to Tux.
|
||||
|
||||
All files are monochrome (`fill="currentColor"`), original per-icon viewBoxes preserved.
|
||||
Licensing: attribution notices live in `LICENSES/` and are folded into
|
||||
`THIRD-PARTY-NOTICES.txt` by `scripts/gen-third-party-notices.py`. The marks are
|
||||
trademarks of their respective owners; they are used here nominatively — to *identify*
|
||||
the operating system a host runs, the standard practice in this ecosystem — and imply no
|
||||
affiliation or endorsement.
|
||||
@@ -0,0 +1,2 @@
|
||||
<!-- apple — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaApple. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 640 B |
@@ -0,0 +1,2 @@
|
||||
<!-- arch — from Simple Icons (CC0 1.0), via react-icons SiArchlinux. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.39.605C10.376 3.092 9.764 4.72 8.635 7.132c.693.734 1.543 1.589 2.923 2.554-1.484-.61-2.496-1.224-3.252-1.86C6.86 10.842 4.596 15.138 0 23.395c3.612-2.085 6.412-3.37 9.021-3.862a6.61 6.61 0 01-.171-1.547l.003-.115c.058-2.315 1.261-4.095 2.687-3.973 1.426.12 2.534 2.096 2.478 4.409a6.52 6.52 0 01-.146 1.243c2.58.505 5.352 1.787 8.914 3.844-.702-1.293-1.33-2.459-1.929-3.57-.943-.73-1.926-1.682-3.933-2.713 1.38.359 2.367.772 3.137 1.234-6.09-11.334-6.582-12.84-8.67-17.74zM22.898 21.36v-.623h-.234v-.084h.562v.084h-.234v.623h.331v-.707h.142l.167.5.034.107a2.26 2.26 0 01.038-.114l.17-.493H24v.707h-.091v-.593l-.206.593h-.084l-.205-.602v.602h-.091"/></svg>
|
||||
|
After Width: | Height: | Size: 841 B |
@@ -0,0 +1,2 @@
|
||||
<!-- debian — from Simple Icons (CC0 1.0), via react-icons SiDebian. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.88 12.685c-.4 0 .08.2.601.28.14-.1.27-.22.39-.33a3.001 3.001 0 01-.99.05m2.14-.53c.23-.33.4-.69.47-1.06-.06.27-.2.5-.33.73-.75.47-.07-.27 0-.56-.8 1.01-.11.6-.14.89m.781-2.05c.05-.721-.14-.501-.2-.221.07.04.13.5.2.22M12.38.31c.2.04.45.07.42.12.23-.05.28-.1-.43-.12m.43.12l-.15.03.14-.01V.43m6.633 9.944c.02.64-.2.95-.38 1.5l-.35.181c-.28.54.03.35-.17.78-.44.39-1.34 1.22-1.62 1.301-.201 0 .14-.25.19-.34-.591.4-.481.6-1.371.85l-.03-.06c-2.221 1.04-5.303-1.02-5.253-3.842-.03.17-.07.13-.12.2a3.551 3.552 0 012.001-3.501 3.361 3.362 0 013.732.48 3.341 3.342 0 00-2.721-1.3c-1.18.01-2.281.76-2.651 1.57-.6.38-.67 1.47-.93 1.661-.361 2.601.66 3.722 2.38 5.042.27.19.08.21.12.35a4.702 4.702 0 01-1.53-1.16c.23.33.47.66.8.91-.55-.18-1.27-1.3-1.48-1.35.93 1.66 3.78 2.921 5.261 2.3a6.203 6.203 0 01-2.33-.28c-.33-.16-.77-.51-.7-.57a5.802 5.803 0 005.902-.84c.44-.35.93-.94 1.07-.95-.2.32.04.16-.12.44.44-.72-.2-.3.46-1.24l.24.33c-.09-.6.74-1.321.66-2.262.19-.3.2.3 0 .97.29-.74.08-.85.15-1.46.08.2.18.42.23.63-.18-.7.2-1.2.28-1.6-.09-.05-.28.3-.32-.53 0-.37.1-.2.14-.28-.08-.05-.26-.32-.38-.861.08-.13.22.33.34.34-.08-.42-.2-.75-.2-1.08-.34-.68-.12.1-.4-.3-.34-1.091.3-.25.34-.74.54.77.84 1.96.981 2.46-.1-.6-.28-1.2-.49-1.76.16.07-.26-1.241.21-.37A7.823 7.824 0 0017.702 1.6c.18.17.42.39.33.42-.75-.45-.62-.48-.73-.67-.61-.25-.65.02-1.06 0C15.082.73 14.862.8 13.8.4l.05.23c-.77-.25-.9.1-1.73 0-.05-.04.27-.14.53-.18-.741.1-.701-.14-1.431.03.17-.13.36-.21.55-.32-.6.04-1.44.35-1.18.07C9.6.68 7.847 1.3 6.867 2.22L6.838 2c-.45.54-1.96 1.611-2.08 2.311l-.131.03c-.23.4-.38.85-.57 1.261-.3.52-.45.2-.4.28-.6 1.22-.9 2.251-1.16 3.102.18.27 0 1.65.07 2.76-.3 5.463 3.84 10.776 8.363 12.006.67.23 1.65.23 2.49.25-.99-.28-1.12-.15-2.08-.49-.7-.32-.85-.7-1.34-1.13l.2.35c-.971-.34-.57-.42-1.361-.67l.21-.27c-.31-.03-.83-.53-.97-.81l-.34.01c-.41-.501-.63-.871-.61-1.161l-.111.2c-.13-.21-1.52-1.901-.8-1.511-.13-.12-.31-.2-.5-.55l.14-.17c-.35-.44-.64-1.02-.62-1.2.2.24.32.3.45.33-.88-2.172-.93-.12-1.601-2.202l.15-.02c-.1-.16-.18-.34-.26-.51l.06-.6c-.63-.74-.18-3.102-.09-4.402.07-.54.53-1.1.88-1.981l-.21-.04c.4-.71 2.341-2.872 3.241-2.761.43-.55-.09 0-.18-.14.96-.991 1.26-.7 1.901-.88.7-.401-.6.16-.27-.151 1.2-.3.85-.7 2.421-.85.16.1-.39.14-.52.26 1-.49 3.151-.37 4.562.27 1.63.77 3.461 3.011 3.531 5.132l.08.02c-.04.85.13 1.821-.17 2.711l.2-.42M9.54 13.236l-.05.28c.26.35.47.73.8 1.01-.24-.47-.42-.66-.75-1.3m.62-.02c-.14-.15-.22-.34-.31-.52.08.32.26.6.43.88l-.12-.36m10.945-2.382l-.07.15c-.1.76-.34 1.511-.69 2.212.4-.73.65-1.541.75-2.362M12.45.12c.27-.1.66-.05.95-.12-.37.03-.74.05-1.1.1l.15.02M3.006 5.142c.07.57-.43.8.11.42.3-.66-.11-.18-.1-.42m-.64 2.661c.12-.39.15-.62.2-.84-.35.44-.17.53-.2.83"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- fedora — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaFedora. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor"><path d="M225 32C101.3 31.7.8 131.7.4 255.4L0 425.7a53.6 53.6 0 0 0 53.6 53.9l170.2.4c123.7.3 224.3-99.7 224.6-223.4S348.7 32.3 225 32zm169.8 157.2L333 126.6c2.3-4.7 3.8-9.2 3.8-14.3v-1.6l55.2 56.1a101 101 0 0 1 2.8 22.4zM331 94.3a106.06 106.06 0 0 1 58.5 63.8l-54.3-54.6a26.48 26.48 0 0 0-4.2-9.2zM118.1 247.2a49.66 49.66 0 0 0-7.7 11.4l-8.5-8.5a85.78 85.78 0 0 1 16.2-2.9zM97 251.4l11.8 11.9-.9 8a34.74 34.74 0 0 0 2.4 12.5l-27-27.2a80.6 80.6 0 0 1 13.7-5.2zm-18.2 7.4l38.2 38.4a53.17 53.17 0 0 0-14.1 4.7L67.6 266a107 107 0 0 1 11.2-7.2zm-15.2 9.8l35.3 35.5a67.25 67.25 0 0 0-10.5 8.5L53.5 278a64.33 64.33 0 0 1 10.1-9.4zm-13.3 12.3l34.9 35a56.84 56.84 0 0 0-7.7 11.4l-35.8-35.9c2.8-3.8 5.7-7.2 8.6-10.5zm-11 14.3l36.4 36.6a48.29 48.29 0 0 0-3.6 15.2l-39.5-39.8a99.81 99.81 0 0 1 6.7-12zm-8.8 16.3l41.3 41.8a63.47 63.47 0 0 0 6.7 26.2L25.8 326c1.4-4.9 2.9-9.6 4.7-14.5zm-7.9 43l61.9 62.2a31.24 31.24 0 0 0-3.6 14.3v1.1l-55.4-55.7a88.27 88.27 0 0 1-2.9-21.9zm5.3 30.7l54.3 54.6a28.44 28.44 0 0 0 4.2 9.2 106.32 106.32 0 0 1-58.5-63.8zm-5.3-37a80.69 80.69 0 0 1 2.1-17l72.2 72.5a37.59 37.59 0 0 0-9.9 8.7zm253.3-51.8l-42.6-.1-.1 56c-.2 69.3-64.4 115.8-125.7 102.9-5.7 0-19.9-8.7-19.9-24.2a24.89 24.89 0 0 1 24.5-24.6c6.3 0 6.3 1.6 15.7 1.6a55.91 55.91 0 0 0 56.1-55.9l.1-47c0-4.5-4.5-9-8.9-9l-33.6-.1c-32.6-.1-32.5-49.4.1-49.3l42.6.1.1-56a105.18 105.18 0 0 1 105.6-105 86.35 86.35 0 0 1 20.2 2.3c11.2 1.8 19.9 11.9 19.9 24 0 15.5-14.9 27.8-30.3 23.9-27.4-5.9-65.9 14.4-66 54.9l-.1 47a8.94 8.94 0 0 0 8.9 9l33.6.1c32.5.2 32.4 49.5-.2 49.4zm23.5-.3a35.58 35.58 0 0 0 7.6-11.4l8.5 8.5a102 102 0 0 1-16.1 2.9zm21-4.2L308.6 280l.9-8.1a34.74 34.74 0 0 0-2.4-12.5l27 27.2a74.89 74.89 0 0 1-13.7 5.3zm18-7.4l-38-38.4c4.9-1.1 9.6-2.4 13.7-4.7l36.2 35.9c-3.8 2.5-7.9 5-11.9 7.2zm15.5-9.8l-35.3-35.5a61.06 61.06 0 0 0 10.5-8.5l34.9 35a124.56 124.56 0 0 1-10.1 9zm13.2-12.3l-34.9-35a63.18 63.18 0 0 0 7.7-11.4l35.8 35.9a130.28 130.28 0 0 1-8.6 10.5zm11-14.3l-36.4-36.6a48.29 48.29 0 0 0 3.6-15.2l39.5 39.8a87.72 87.72 0 0 1-6.7 12zm13.5-30.9a140.63 140.63 0 0 1-4.7 14.3L345.6 190a58.19 58.19 0 0 0-7.1-26.2zm1-5.6l-71.9-72.1a32 32 0 0 0 9.9-9.2l64.3 64.7a90.93 90.93 0 0 1-2.3 16.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- linux — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaLinux. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor"><path d="M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- nixos — from Simple Icons (CC0 1.0), via react-icons SiNixos. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M7.352 1.592l-1.364.002L5.32 2.75l1.557 2.713-3.137-.008-1.32 2.34H14.11l-1.353-2.332-3.192-.006-2.214-3.865zm6.175 0l-2.687.025 5.846 10.127 1.341-2.34-1.59-2.765 2.24-3.85-.683-1.182h-1.336l-1.57 2.705-1.56-2.72zm6.887 4.195l-5.846 10.125 2.696-.008 1.601-2.76 4.453.016.682-1.183-.666-1.157-3.13-.008L21.778 8.1l-1.365-2.313zM9.432 8.086l-2.696.008-1.601 2.76-4.453-.016L0 12.02l.666 1.157 3.13.008-1.575 2.71 1.365 2.315L9.432 8.086zM7.33 12.25l-.006.01-.002-.004-1.342 2.34 1.59 2.765-2.24 3.85.684 1.182H7.35l.004-.006h.001l1.567-2.698 1.558 2.72 2.688-.026-.004-.006h.01L7.33 12.25zm2.55 3.93l1.354 2.332 3.192.006 2.215 3.865 1.363-.002.668-1.156-1.557-2.713 3.137.008 1.32-2.34H9.881Z"/></svg>
|
||||
|
After Width: | Height: | Size: 880 B |
@@ -0,0 +1,2 @@
|
||||
<!-- opensuse — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaSuse. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor"><path d="M471.08 102.66s-.3 18.3-.3 20.3c-9.1-3-74.4-24.1-135.7-26.3-51.9-1.8-122.8-4.3-223 57.3-19.4 12.4-73.9 46.1-99.6 109.7C7 277-.12 307 7 335.06a111 111 0 0 0 16.5 35.7c17.4 25 46.6 41.6 78.1 44.4 44.4 3.9 78.1-16 90-53.3 8.2-25.8 0-63.6-31.5-82.9-25.6-15.7-53.3-12.1-69.2-1.6-13.9 9.2-21.8 23.5-21.6 39.2.3 27.8 24.3 42.6 41.5 42.6a49 49 0 0 0 15.8-2.7c6.5-1.8 13.3-6.5 13.3-14.9 0-12.1-11.6-14.8-16.8-13.9-2.9.5-4.5 2-11.8 2.4-2-.2-12-3.1-12-14V316c.2-12.3 13.2-18 25.5-16.9 32.3 2.8 47.7 40.7 28.5 65.7-18.3 23.7-76.6 23.2-99.7-20.4-26-49.2 12.7-111.2 87-98.4 33.2 5.7 83.6 35.5 102.4 104.3h45.9c-5.7-17.6-8.9-68.3 42.7-68.3 56.7 0 63.9 39.9 79.8 68.3H460c-12.8-18.3-21.7-38.7-18.9-55.8 5.6-33.8 39.7-18.4 82.4-17.4 66.5.4 102.1-27 103.1-28 3.7-3.1 6.5-15.8 7-17.7 1.3-5.1-3.2-2.4-3.2-2.4-8.7 5.2-30.5 15.2-50.9 15.6-25.3.5-76.2-25.4-81.6-28.2-.3-.4.1 1.2-11-25.5 88.4 58.3 118.3 40.5 145.2 21.7.8-.6 4.3-2.9 3.6-5.7-13.8-48.1-22.4-62.7-34.5-69.6-37-21.6-125-34.7-129.2-35.3.1-.1-.9-.3-.9.7zm60.4 72.8a37.54 37.54 0 0 1 38.9-36.3c33.4 1.2 48.8 42.3 24.4 65.2-24.2 22.7-64.4 4.6-63.3-28.9zm38.6-25.3a26.27 26.27 0 1 0 25.4 27.2 26.19 26.19 0 0 0-25.4-27.2zm4.3 28.8c-15.4 0-15.4-15.6 0-15.6s15.4 15.64 0 15.64z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- steam — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaSteam. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" fill="currentColor"><path d="M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 937 B |
@@ -0,0 +1,2 @@
|
||||
<!-- ubuntu — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaUbuntu. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" fill="currentColor"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm52.7 93c8.8-15.2 28.3-20.5 43.5-11.7 15.3 8.8 20.5 28.3 11.7 43.6-8.8 15.2-28.3 20.5-43.5 11.7-15.3-8.9-20.5-28.4-11.7-43.6zM87.4 287.9c-17.6 0-31.9-14.3-31.9-31.9 0-17.6 14.3-31.9 31.9-31.9 17.6 0 31.9 14.3 31.9 31.9 0 17.6-14.3 31.9-31.9 31.9zm28.1 3.1c22.3-17.9 22.4-51.9 0-69.9 8.6-32.8 29.1-60.7 56.5-79.1l23.7 39.6c-51.5 36.3-51.5 112.5 0 148.8L172 370c-27.4-18.3-47.8-46.3-56.5-79zm228.7 131.7c-15.3 8.8-34.7 3.6-43.5-11.7-8.8-15.3-3.6-34.8 11.7-43.6 15.2-8.8 34.7-3.6 43.5 11.7 8.8 15.3 3.6 34.8-11.7 43.6zm.3-69.5c-26.7-10.3-56.1 6.6-60.5 35-5.2 1.4-48.9 14.3-96.7-9.4l22.5-40.3c57 26.5 123.4-11.7 128.9-74.4l46.1.7c-2.3 34.5-17.3 65.5-40.3 88.4zm-5.9-105.3c-5.4-62-71.3-101.2-128.9-74.4l-22.5-40.3c47.9-23.7 91.5-10.8 96.7-9.4 4.4 28.3 33.8 45.3 60.5 35 23.1 22.9 38 53.9 40.2 88.5l-46 .6z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- windows — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaWindows. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor"><path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 344 B |
@@ -18,6 +18,9 @@
|
||||
//! - `mac` — the host's wake-capable NIC MAC(s) (comma-separated, routed NIC first), which a client
|
||||
//! persists so it can Wake-on-LAN this host after it sleeps. Advisory/unauthenticated (a wrong
|
||||
//! MAC only makes a wake fail). Omitted when none can be read.
|
||||
//! - `os` — the host's OS identity chain (`windows` | `macos` | `linux[/<family>][/<id>]`, e.g.
|
||||
//! `linux/fedora/bazzite` — see [`crate::osinfo`]), so a client can show an OS icon on the host
|
||||
//! card. Advisory/unauthenticated like `mac`: a wrong value only draws a wrong icon.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use mdns_sd::{ServiceDaemon, ServiceInfo};
|
||||
@@ -87,6 +90,9 @@ pub struct Advert {
|
||||
/// `require_pairing` tells a discovering client whether it must pair before it can stream;
|
||||
/// `mgmt_port` is the management API's port (`Some` when this host serves one — the client browses
|
||||
/// the library there over mTLS on the advertised IP), `None` for a host with no mgmt API.
|
||||
// One parameter per TXT key, single call site — a params struct would just restate the
|
||||
// module doc's key list with extra ceremony.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn advertise_native(
|
||||
hostname: &str,
|
||||
ip: IpAddr,
|
||||
@@ -95,6 +101,7 @@ pub fn advertise_native(
|
||||
require_pairing: bool,
|
||||
uniqueid: &str,
|
||||
mgmt_port: Option<u16>,
|
||||
os_chain: &str,
|
||||
) -> Result<Advert> {
|
||||
let daemon = ServiceDaemon::new().context("create mDNS daemon")?;
|
||||
// `hostname` is the DISPLAY name (the instance label clients read back); the A-record target
|
||||
@@ -116,6 +123,10 @@ pub fn advertise_native(
|
||||
if let Some(mgmt) = mgmt_port {
|
||||
props.insert("mgmt".into(), mgmt.to_string());
|
||||
}
|
||||
// `os` — advisory OS-identity chain for the client's host-card icon (see module doc).
|
||||
if !os_chain.is_empty() {
|
||||
props.insert("os".into(), os_chain.to_string());
|
||||
}
|
||||
// `mac` — the host's wake-capable NIC MAC(s), comma-separated `aa:bb:cc:dd:ee:ff`, routed NIC
|
||||
// first. A client persists these while the host is awake so it can send a Wake-on-LAN magic
|
||||
// packet to wake it later (when it's asleep and no longer advertising). Unauthenticated like
|
||||
|
||||
@@ -119,17 +119,25 @@ pub struct Host {
|
||||
pub local_ip: IpAddr,
|
||||
pub http_port: u16,
|
||||
pub https_port: u16,
|
||||
/// OS identity chain (`windows` | `macos` | `linux[/<family>][/<id>]`), advertised in the
|
||||
/// mDNS `os=` TXT record and `HostInfo.os` so clients can show an OS icon.
|
||||
pub os_chain: String,
|
||||
/// Human-readable OS name (os-release `PRETTY_NAME`), surfaced as `HostInfo.os_name` only.
|
||||
pub os_name: String,
|
||||
// Pairing state (server cert, paired client certs) lands in the next P1.1 slice.
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn detect() -> Result<Host> {
|
||||
let os = crate::osinfo::detect();
|
||||
Ok(Host {
|
||||
hostname: hostname_string(),
|
||||
uniqueid: load_or_create_uniqueid()?,
|
||||
local_ip: primary_local_ip().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
http_port: HTTP_PORT,
|
||||
https_port: HTTPS_PORT,
|
||||
os_chain: os.chain.clone(),
|
||||
os_name: os.pretty.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -578,6 +586,8 @@ mod session_tests {
|
||||
local_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
http_port: HTTP_PORT,
|
||||
https_port: HTTPS_PORT,
|
||||
os_chain: "linux".into(),
|
||||
os_name: "Linux".into(),
|
||||
};
|
||||
let identity = cert::ServerIdentity::ephemeral().expect("ephemeral identity");
|
||||
let stats = crate::stats_recorder::StatsRecorder::new(std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -427,6 +427,8 @@ mod tests {
|
||||
local_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
http_port: HTTP_PORT,
|
||||
https_port: HTTPS_PORT,
|
||||
os_chain: "linux".into(),
|
||||
os_name: "Linux".into(),
|
||||
};
|
||||
let identity = super::super::cert::ServerIdentity::ephemeral().expect("ephemeral identity");
|
||||
let stats = crate::stats_recorder::StatsRecorder::new(
|
||||
|
||||
@@ -208,6 +208,8 @@ mod tests {
|
||||
local_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
|
||||
http_port: 47989,
|
||||
https_port: 47984,
|
||||
os_chain: "linux".into(),
|
||||
os_name: "Linux".into(),
|
||||
};
|
||||
let xml = serverinfo_xml(&host, false, false);
|
||||
// The mask is the GPU-aware value (NVENC/no-GPU → the static 65793; a VAAPI host →
|
||||
|
||||
@@ -77,6 +77,7 @@ mod mgmt;
|
||||
mod mgmt_token;
|
||||
mod native;
|
||||
mod native_pairing;
|
||||
mod osinfo;
|
||||
mod pipeline;
|
||||
mod plugins;
|
||||
// Finding a launched game's processes from its store's detect signals — the read side of the
|
||||
|
||||
@@ -39,6 +39,14 @@ pub(crate) struct HostInfo {
|
||||
app_version: String,
|
||||
/// GFE version advertised to Moonlight clients.
|
||||
gfe_version: String,
|
||||
/// OS identity chain, generic → most specific, slash-separated (`windows` | `macos` |
|
||||
/// `linux[/<family>][/<id>]`). A client walks it most-specific-first and shows the first
|
||||
/// token it has an icon for, so an unknown distro still degrades to its family's mark.
|
||||
#[schema(example = "linux/fedora/bazzite")]
|
||||
os: String,
|
||||
/// Human-readable OS name (os-release `PRETTY_NAME`; `"Windows"`/`"macOS"` elsewhere).
|
||||
#[schema(example = "Bazzite 42 (Kinoite)")]
|
||||
os_name: String,
|
||||
/// Codecs the host can encode (NVENC).
|
||||
codecs: Vec<ApiCodec>,
|
||||
/// Whether the GameStream/Moonlight-compat planes are running (`--gamestream`). `false` on the
|
||||
@@ -262,6 +270,8 @@ pub(crate) async fn get_host_info(State(st): State<Arc<MgmtState>>) -> Json<Host
|
||||
abi_version: punktfunk_core::ABI_VERSION,
|
||||
app_version: APP_VERSION.into(),
|
||||
gfe_version: GFE_VERSION.into(),
|
||||
os: h.os_chain.clone(),
|
||||
os_name: h.os_name.clone(),
|
||||
// What this host can ACTUALLY encode on its resolved backend — GPU-aware, straight from the
|
||||
// same capability mask that drives GameStream/QUIC negotiation ([`Codec::host_wire_caps`]).
|
||||
// So an iGPU without AV1 encode won't advertise AV1, a software-only host reports H.264 only,
|
||||
|
||||
@@ -29,6 +29,8 @@ fn test_state() -> Arc<AppState> {
|
||||
local_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
http_port: HTTP_PORT,
|
||||
https_port: HTTPS_PORT,
|
||||
os_chain: "linux/arch/steamos".into(),
|
||||
os_name: "SteamOS".into(),
|
||||
};
|
||||
let identity = ServerIdentity::ephemeral().expect("ephemeral identity");
|
||||
Arc::new(AppState::new(host, identity, test_stats()))
|
||||
@@ -623,6 +625,10 @@ async fn host_info_reports_identity_and_ports() {
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["hostname"], "test-host");
|
||||
assert_eq!(body["uniqueid"], "deadbeef");
|
||||
// OS identity rides along verbatim from the detected Host (chain for the icon walk,
|
||||
// pretty name for the human label).
|
||||
assert_eq!(body["os"], "linux/arch/steamos");
|
||||
assert_eq!(body["os_name"], "SteamOS");
|
||||
assert_eq!(body["ports"]["http"], HTTP_PORT);
|
||||
assert_eq!(body["ports"]["mgmt"], DEFAULT_PORT);
|
||||
// Codecs are GPU-aware (derived from `Codec::host_wire_caps`), so assert against that mask
|
||||
|
||||
@@ -318,6 +318,7 @@ pub(crate) async fn serve(
|
||||
&h.uniqueid,
|
||||
// 0 = standalone `punktfunk1-host` (no mgmt API) → don't advertise an `mgmt` port.
|
||||
(mgmt_port != 0).then_some(mgmt_port),
|
||||
&h.os_chain,
|
||||
)
|
||||
.map_err(|e| tracing::warn!(error = %format!("{e:#}"), "native mDNS advertise failed (continuing)"))
|
||||
.ok(),
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
//! Host OS identity for client-facing surfaces: an icon-friendly **specificity chain** plus a
|
||||
//! human-readable name.
|
||||
//!
|
||||
//! The chain is slash-separated, generic → specific, `family[/like][/id]`:
|
||||
//! `windows`, `macos`, `linux`, `linux/debian/ubuntu`, `linux/fedora/bazzite`,
|
||||
//! `linux/arch/steamos`. Clients walk it most-specific-first and show the first token they have
|
||||
//! art for — a client with no Bazzite mark lands on `fedora`, then generic `linux` — so the host
|
||||
//! always emits the *full* chain and clients need zero distro→parent knowledge. On Linux the
|
||||
//! middle token is the first `ID_LIKE` ancestor we recognize ([`FAMILIES`]; `ID_LIKE` is ordered
|
||||
//! most-similar-first per the os-release spec) and the leaf is `ID` verbatim, so even a distro we
|
||||
//! never heard of still degrades to its family's icon.
|
||||
//!
|
||||
//! The chain rides the mDNS `os=` TXT record ([`crate::discovery`]) and the mgmt API's
|
||||
//! `HostInfo.os`; the pretty name (`PRETTY_NAME`) is REST-only — TXT stays small. Both values are
|
||||
//! advisory identity, same trust posture as the `mac` TXT key: unauthenticated, and a wrong value
|
||||
//! only draws a wrong icon.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// The host's OS identity, detected once per process (os-release is static for its lifetime).
|
||||
pub struct OsInfo {
|
||||
/// `windows` | `macos` | `linux[/<family>][/<id>]` — see the module doc for the grammar.
|
||||
pub chain: String,
|
||||
/// Human-readable OS name (os-release `PRETTY_NAME`; `"Windows"`/`"macOS"` elsewhere).
|
||||
pub pretty: String,
|
||||
}
|
||||
|
||||
/// Detect (once) and return the host's OS identity.
|
||||
pub fn detect() -> &'static OsInfo {
|
||||
static OS: OnceLock<OsInfo> = OnceLock::new();
|
||||
OS.get_or_init(|| {
|
||||
if cfg!(target_os = "windows") {
|
||||
OsInfo {
|
||||
chain: "windows".into(),
|
||||
pretty: "Windows".into(),
|
||||
}
|
||||
} else if cfg!(target_os = "macos") {
|
||||
// The host only runs on Windows/Linux; macOS is the dev-build platform.
|
||||
OsInfo {
|
||||
chain: "macos".into(),
|
||||
pretty: "macOS".into(),
|
||||
}
|
||||
} else {
|
||||
linux_os_info()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `ID_LIKE` ancestors worth keeping as the chain's middle token — the distros clients plausibly
|
||||
/// have icon art for. Anything else (or no `ID_LIKE` at all) yields a two-segment chain and the
|
||||
/// leaf still identifies the distro exactly.
|
||||
const FAMILIES: &[&str] = &[
|
||||
"debian", "ubuntu", "fedora", "rhel", "arch", "opensuse", "suse", "gentoo", "alpine", "nixos",
|
||||
];
|
||||
|
||||
/// Read os-release from its two spec'd locations (`/etc` overrides the `/usr/lib` fallback);
|
||||
/// a host with neither (or unreadable) is simply `linux`.
|
||||
fn linux_os_info() -> OsInfo {
|
||||
["/etc/os-release", "/usr/lib/os-release"]
|
||||
.iter()
|
||||
.find_map(|p| std::fs::read_to_string(p).ok())
|
||||
.map(|s| parse_os_release(&s))
|
||||
.unwrap_or_else(|| OsInfo {
|
||||
chain: "linux".into(),
|
||||
pretty: "Linux".into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Pure os-release parser (compiled on every target so the tests run on any dev box).
|
||||
///
|
||||
/// Grammar per the freedesktop spec subset we need: `KEY=value` lines, values optionally
|
||||
/// `"`/`'`-quoted. `ID`/`ID_LIKE` tokens are lowercased and sanitized before they feed a DNS TXT
|
||||
/// record; `PRETTY_NAME` falls back `NAME` → capitalized `ID` → `"Linux"`.
|
||||
fn parse_os_release(contents: &str) -> OsInfo {
|
||||
let (mut id, mut id_like, mut pretty, mut name) = (None, None, None, None);
|
||||
for line in contents.lines() {
|
||||
let line = line.trim();
|
||||
if let Some(v) = line.strip_prefix("ID=") {
|
||||
id = Some(unquote(v));
|
||||
} else if let Some(v) = line.strip_prefix("ID_LIKE=") {
|
||||
id_like = Some(unquote(v));
|
||||
} else if let Some(v) = line.strip_prefix("PRETTY_NAME=") {
|
||||
pretty = Some(unquote(v));
|
||||
} else if let Some(v) = line.strip_prefix("NAME=") {
|
||||
name = Some(unquote(v));
|
||||
}
|
||||
}
|
||||
|
||||
let id_tok = id.as_deref().and_then(sanitize_token);
|
||||
// First *recognized* ID_LIKE ancestor wins (the list is ordered most-similar-first), skipping
|
||||
// a token that merely repeats ID (some distros write `ID_LIKE` reflexively).
|
||||
let like_tok = id_like
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.split_whitespace()
|
||||
.filter_map(sanitize_token)
|
||||
.find(|t| FAMILIES.contains(&t.as_str()) && Some(t) != id_tok.as_ref());
|
||||
|
||||
let mut chain = String::from("linux");
|
||||
for tok in [&like_tok, &id_tok].into_iter().flatten() {
|
||||
chain.push('/');
|
||||
chain.push_str(tok);
|
||||
}
|
||||
|
||||
let pretty = [pretty, name]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|s| s.trim().to_string())
|
||||
.find(|s| !s.is_empty())
|
||||
.or_else(|| id_tok.as_deref().map(capitalize))
|
||||
.unwrap_or_else(|| "Linux".into());
|
||||
|
||||
OsInfo { chain, pretty }
|
||||
}
|
||||
|
||||
/// Strip one matching pair of surrounding `"` or `'` quotes.
|
||||
fn unquote(v: &str) -> String {
|
||||
let v = v.trim();
|
||||
for q in ['"', '\''] {
|
||||
if let Some(inner) = v.strip_prefix(q).and_then(|s| s.strip_suffix(q)) {
|
||||
return inner.to_string();
|
||||
}
|
||||
}
|
||||
v.to_string()
|
||||
}
|
||||
|
||||
/// Lowercase and reduce an `ID`/`ID_LIKE` token to TXT-safe `[a-z0-9._-]`, capped at 32 chars;
|
||||
/// `None` when nothing survives (a hostile or garbage value must not reach the advert).
|
||||
fn sanitize_token(raw: &str) -> Option<String> {
|
||||
let tok: String = raw
|
||||
.to_lowercase()
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '_' | '-'))
|
||||
.take(32)
|
||||
.collect();
|
||||
(!tok.is_empty()).then_some(tok)
|
||||
}
|
||||
|
||||
/// `ubuntu` → `Ubuntu` (ASCII-only; the fallback label when os-release names are absent).
|
||||
fn capitalize(s: &str) -> String {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parsed(contents: &str) -> (String, String) {
|
||||
let info = parse_os_release(contents);
|
||||
(info.chain, info.pretty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ubuntu_chains_through_debian() {
|
||||
let (chain, pretty) = parsed(
|
||||
"PRETTY_NAME=\"Ubuntu 24.04.2 LTS\"\nNAME=\"Ubuntu\"\nID=ubuntu\nID_LIKE=debian\n",
|
||||
);
|
||||
assert_eq!(chain, "linux/debian/ubuntu");
|
||||
assert_eq!(pretty, "Ubuntu 24.04.2 LTS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bazzite_quoted_id_like() {
|
||||
let (chain, pretty) =
|
||||
parsed("ID=bazzite\nID_LIKE=\"fedora\"\nPRETTY_NAME=\"Bazzite 42 (Kinoite)\"\n");
|
||||
assert_eq!(chain, "linux/fedora/bazzite");
|
||||
assert_eq!(pretty, "Bazzite 42 (Kinoite)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steamos_is_arch_family() {
|
||||
let (chain, _) = parsed("ID=steamos\nID_LIKE=arch\nPRETTY_NAME=\"SteamOS\"\n");
|
||||
assert_eq!(chain, "linux/arch/steamos");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cachyos_is_arch_family() {
|
||||
let (chain, _) = parsed("ID=cachyos\nID_LIKE=\"arch\"\nNAME=\"CachyOS Linux\"\n");
|
||||
assert_eq!(chain, "linux/arch/cachyos");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nixos_without_id_like_is_two_segments() {
|
||||
let (chain, _) = parsed("ID=nixos\nPRETTY_NAME=\"NixOS 25.05 (Warbler)\"\n");
|
||||
assert_eq!(chain, "linux/nixos");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_root_distro() {
|
||||
let (chain, _) = parsed("ID=arch\nNAME=\"Arch Linux\"\n");
|
||||
assert_eq!(chain, "linux/arch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_ancestor_id_like_takes_first_recognized() {
|
||||
// centos: ID_LIKE is most-similar-first — rhel wins over fedora.
|
||||
let (chain, _) = parsed("ID=\"centos\"\nID_LIKE=\"rhel fedora\"\n");
|
||||
assert_eq!(chain, "linux/rhel/centos");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_takes_ubuntu_over_debian() {
|
||||
let (chain, _) = parsed("ID=pop\nID_LIKE=\"ubuntu debian\"\n");
|
||||
assert_eq!(chain, "linux/ubuntu/pop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflexive_id_like_is_skipped() {
|
||||
let (chain, _) = parsed("ID=fedora\nID_LIKE=fedora\n");
|
||||
assert_eq!(chain, "linux/fedora");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrecognized_id_like_is_dropped_leaf_kept() {
|
||||
let (chain, _) = parsed("ID=chimera\nID_LIKE=frontier\n");
|
||||
assert_eq!(chain, "linux/chimera");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_and_empty_fall_back_to_linux() {
|
||||
assert_eq!(parsed("").0, "linux");
|
||||
assert_eq!(parsed("not an os-release file at all\n===\n").0, "linux");
|
||||
assert_eq!(parsed("").1, "Linux");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_id_is_sanitized_for_txt() {
|
||||
// Quotes/spaces/slashes must never reach the TXT record or split the chain.
|
||||
let (chain, _) = parsed("ID=\"Ubu ntu/EVIL=1\"\n");
|
||||
assert_eq!(chain, "linux/ubuntuevil1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlong_id_is_capped() {
|
||||
let long = "x".repeat(80);
|
||||
let (chain, _) = parsed(&format!("ID={long}\n"));
|
||||
assert_eq!(chain, format!("linux/{}", "x".repeat(32)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_falls_back_name_then_id() {
|
||||
assert_eq!(
|
||||
parsed("ID=debian\nNAME=\"Debian GNU/Linux\"\n").1,
|
||||
"Debian GNU/Linux"
|
||||
);
|
||||
assert_eq!(parsed("ID=debian\n").1, "Debian");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_quoted_values() {
|
||||
let (chain, pretty) = parsed("ID='opensuse-tumbleweed'\nID_LIKE='opensuse suse'\nPRETTY_NAME='openSUSE Tumbleweed'\n");
|
||||
assert_eq!(chain, "linux/opensuse/opensuse-tumbleweed");
|
||||
assert_eq!(pretty, "openSUSE Tumbleweed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf_and_padding_tolerated() {
|
||||
let (chain, _) = parsed(" ID=ubuntu \r\n\tID_LIKE=debian\r\n");
|
||||
assert_eq!(chain, "linux/debian/ubuntu");
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,14 @@ VENDORED_TREES = [
|
||||
("Vulkan-Headers (vendored, crates/pyrowave-sys)",
|
||||
"crates/pyrowave-sys/vendor/pyrowave/Granite/third_party/khronos/vulkan-headers/LICENSE.md",
|
||||
"https://github.com/KhronosGroup/Vulkan-Headers"),
|
||||
# OS brand marks for the host-card OS icon (assets/os-icons/, CC BY 4.0 / CC0 —
|
||||
# see assets/os-icons/README.md; clients embed per-platform derivatives).
|
||||
("Font Awesome Free brand icons (vendored, assets/os-icons)",
|
||||
"assets/os-icons/LICENSES/font-awesome-brands.txt",
|
||||
"https://fontawesome.com"),
|
||||
("Simple Icons (vendored, assets/os-icons)",
|
||||
"assets/os-icons/LICENSES/simple-icons.txt",
|
||||
"https://simpleicons.org"),
|
||||
]
|
||||
|
||||
|
||||
|
||||