diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eb047ea..8e19021 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build run: cargo build --verbose diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 247ad43..2f508d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,123 +1,101 @@ -# From https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml -# Which is also via https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ -# ...both of which are very good. -# -# I'm sure I don't need half the stuff I have in here (around cargo -# customization and whatnot) but. -# name: release on: + workflow_dispatch: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" +permissions: + contents: write + jobs: create_release: name: Create release - runs-on: ubuntu-22.04 - - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} + runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + + - name: Get the release version + if: env.VERSION == '' + run: echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + - name: Show the version + run: | + echo "version is: $VERSION" + - name: Create GitHub release - id: create_release - uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: true + run: gh release create $VERSION --draft --verify-tag --title $VERSION - release_assets: - name: Release assets + outputs: + version: ${{ env.VERSION }} + + build_release: + name: Build all the stuff needs: ['create_release'] # We need to know the upload URL runs-on: ${{ matrix.os }} # We run many different builds env: - # For some builds, we use cross to test on 32-bit and big-endian - # systems. - CARGO: cargo - # When CARGO is set to CROSS, this is set to `--target matrix.target`. - TARGET_FLAGS: "" - # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. - TARGET_DIR: ./target # Emit backtraces on panics. RUST_BACKTRACE: 1 - # Build static releases with PCRE2. - PCRE2_SYS_STATIC: 1 strategy: - # just an example matrix + fail-fast: false matrix: - build: ['linux', 'macos', 'windows'] + build: ['linux', 'debian', 'macos', 'arm-macos', 'windows'] include: - build: linux os: ubuntu-22.04 - rust: nightly target: x86_64-unknown-linux-musl + packages: apt + + - build: debian + os: ubuntu-22.04 + target: x86_64-unknown-linux-musl + packages: apt - build: macos - os: macos-12 - rust: nightly + os: macos-latest target: x86_64-apple-darwin + packages: brew + + - build: arm-macos + os: macos-latest + target: aarch64-apple-darwin + packages: brew - build: windows os: windows-2022 - rust: nightly target: x86_64-pc-windows-msvc + packages: none steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Install Rust - uses: dtolnay/rust-toolchain@master + - name: Install packages (linux) + if: matrix.packages == 'apt' + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + - name: Install packages (macos) + if: matrix.packages == 'brew' + run: | + brew update + brew install pandoc + + - name: Install rust + uses: dtolnay/rust-toolchain@stable with: - toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - - name: Use Cross + - name: Run the release automation shell: bash - run: | - cargo install cross - echo "CARGO=cross" >> $GITHUB_ENV - echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV - echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - - - name: Build release binary - run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} - - - name: Strip release binary (linux and macos) - if: matrix.build == 'linux' || matrix.build == 'macos' - run: | - strip "target/${{ matrix.target }}/release/fwd" - strip "target/${{ matrix.target }}/release/fwd-browse" - - - name: Build archive - shell: bash - run: | - staging="fwd-${{ matrix.target }}" - mkdir -p "$staging" - - if [ "${{ matrix.os }}" = "windows-2022" ]; then - cp "target/${{ matrix.target }}/release/fwd.exe" "$staging/" - 7z a "$staging.zip" "$staging" - echo "ASSET=$staging.zip" >> $GITHUB_ENV - else - cp "target/${{ matrix.target }}/release/fwd" "$staging/" - cp "target/${{ matrix.target }}/release/fwd-browse" "$staging/" - tar czf "$staging.tar.gz" "$staging" - echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV - fi - - - name: Upload release archive - uses: actions/upload-release-asset@v1.0.2 env: + RELEASE_TAG: ${{ needs.create_release.outputs.version }} + BUILD: ${{ matrix.build }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: ${{ env.ASSET }} - asset_path: ${{ env.ASSET }} - asset_content_type: application/octet-stream + run: python3 release.py diff --git a/Cargo.lock b/Cargo.lock index 1a5d73a..3499c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -17,6 +17,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -34,9 +52,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "assert_matches" @@ -46,15 +64,15 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -72,10 +90,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "bumpalo" -version = "3.14.0" +name = "bitflags" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -85,9 +115,35 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix 0.38.34", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.34", + "wayland-backend", + "wayland-client", +] [[package]] name = "cassowary" @@ -96,14 +152,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] -name = "cc" -version = "1.0.83" +name = "castaway" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ - "libc", + "rustversion", ] +[[package]] +name = "cc" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" + [[package]] name = "cfg-if" version = "1.0.0" @@ -112,43 +174,96 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.48.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "copypasta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", ] [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] -name = "crossterm" -version = "0.25.0" +name = "crossbeam-utils" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "crossterm_winapi", "futures-core", - "libc", "mio", "parking_lot", + "rustix 0.38.34", "signal-hook", "signal-hook-mio", "winapi", @@ -164,71 +279,190 @@ dependencies = [ ] [[package]] -name = "errno" -version = "0.3.7" +name = "cursor-icon" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" dependencies = [ - "libc", - "windows-sys 0.48.0", + "cfg-if", + "dirs-sys-next", ] [[package]] -name = "flate2" -version = "1.0.28" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "env_filter", + "log", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "flate2" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "fwd" -version = "0.7.0" +version = "0.9.2" dependencies = [ "anyhow", "assert_matches", "bytes", + "copypasta", "crossterm", - "home", + "directories-next", + "env_logger", "indoc", + "libc", "log", "open", + "pretty_assertions", "procfs", - "tempdir", + "rand", + "ratatui", + "tempfile", "thiserror", "tokio", "tokio-stream", "toml", - "tui", - "users", - "xdg", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", ] [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[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.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" @@ -236,20 +470,11 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" -dependencies = [ - "windows-sys 0.48.0", -] - [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -274,37 +499,88 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] [[package]] -name = "js-sys" -version = "0.3.65" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "lazy-bytes-cast" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] [[package]] name = "linux-raw-sys" @@ -313,10 +589,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] -name = "lock_api" -version = "0.4.11" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -324,70 +606,117 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ - "hermit-abi", - "libc", + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", ] [[package]] name = "object" -version = "0.32.1" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" @@ -401,9 +730,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -411,17 +740,23 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -430,15 +765,55 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -449,84 +824,109 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", "chrono", "flate2", "hex", "lazy_static", - "rustix", + "rustix 0.36.17", +] + +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.4.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "fuchsia-cprng", "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", + "rand_chacha", + "rand_core", ] [[package]] -name = "rand_core" +name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "rand_core 0.4.2", + "ppv-lite86", + "rand_core", ] [[package]] name = "rand_core" -version = "0.4.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] -name = "rdrand" -version = "0.4.0" +name = "ratatui" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" dependencies = [ - "rand_core 0.3.1", + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "redox_users" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "winapi", + "getrandom", + "libredox", + "thiserror", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -534,14 +934,45 @@ version = "0.36.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.4", "windows-sys 0.45.0", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -550,18 +981,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" dependencies = [ "proc-macro2", "quote", @@ -580,9 +1011,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", @@ -591,34 +1022,107 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.34", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", ] [[package]] name = "syn" -version = "2.0.39" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -626,29 +1130,32 @@ dependencies = [ ] [[package]] -name = "tempdir" -version = "0.3.7" +name = "tempfile" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ - "rand", - "remove_dir_all", + "cfg-if", + "fastrand", + "once_cell", + "rustix 0.38.34", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -657,27 +1164,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.34.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -686,9 +1192,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -705,18 +1211,21 @@ dependencies = [ ] [[package]] -name = "tui" -version = "0.19.0" +name = "tracing" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "bitflags", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", + "pin-project-lite", + "tracing-core", ] +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -725,25 +1234,32 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] -name = "users" -version = "0.11.0" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" -dependencies = [ - "libc", - "log", -] +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -753,19 +1269,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -778,9 +1295,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -788,9 +1305,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -801,9 +1318,105 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wayland-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.34", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +dependencies = [ + "bitflags 2.6.0", + "rustix 0.38.34", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.34", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] [[package]] name = "winapi" @@ -829,11 +1442,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -869,6 +1482,24 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -899,6 +1530,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -911,6 +1558,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -923,6 +1576,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -935,6 +1594,18 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -947,6 +1618,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -959,6 +1636,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -971,6 +1654,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -984,7 +1673,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "xdg" -version = "2.5.2" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.34", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 3e5ff60..bed9327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,14 @@ [package] name = "fwd" -version = "0.7.0" +version = "0.9.2" +authors = ["John Doty "] edition = "2021" license = "MIT" -description = "Automatically forward ports to a remote server over ssh" +description = "Automatically forward ports to a remote server" +readme = "README.md" +documentation = "https://github.com/DeCarabas/fwd" homepage = "https://github.com/DeCarabas/fwd" repository = "https://github.com/DeCarabas/fwd" -readme = "README.md" [[bin]] name = "fwd-browse" @@ -16,24 +18,47 @@ bench = false [dependencies] anyhow = "1.0" bytes = "1" -crossterm = { version = "0.25", features = ["event-stream"] } -home = "0.5.4" +copypasta = "0.10.1" +crossterm = { version = "0.28.1", features = ["event-stream"] } +directories-next = "2" +env_logger = { version = "0.11.5", default-features = false } indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" +rand = "0.8.5" +ratatui = "0.28.0" thiserror = "1.0" -tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread"] } +tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] } tokio-stream = "0.1" toml = "0.5" -tui = "0.19" -xdg = "2" + [dev-dependencies] assert_matches = "1" -tempdir = "0.3" +pretty_assertions = "1" +tempfile = "3" [target.'cfg(target_os="linux")'.dependencies] procfs = "0.14.1" [target.'cfg(target_family="unix")'.dependencies] -users = "0.11" +libc = "0.2.155" + +[package.metadata.deb] +section = "net" +depends = [] # No auto deps? +assets = [ + ["target/release/fwd", "usr/bin/", "755"], + ["target/release/fwd-browse", "usr/bin/", "755"], + ["LICENSE", "usr/share/doc/fwd/", "644"], + ["README.md", "usr/share/doc/fwd/README", "644"], + # The man page is automatically generated by fwd's build process. See + # release.py for details. + ["target/release/fwd.1", "usr/share/man/man1/fwd.1", "644"], +] +extended-description = """\ +fwd enumerates the listening ports the remote server and automatically listens +for connections on the same ports on the local machine. When fwd receives a +connection on the local machine, it automatically forwards that connection to +the remote machine. +""" diff --git a/LICENSE b/LICENSE index 7857f35..630f272 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2022 John Doty +Copyright 2024 John Doty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1e3c07d..94a4337 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A port-forwarding utility. Here's how it works: 1. Get the latest [release](https://github.com/DeCarabas/fwd/releases) of `fwd` -2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`) +2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`, or `.local/bin`) 3. You install `fwd` on the client (like your laptop) 4. You run `fwd` on the client to connect to the server, like so: @@ -13,6 +13,9 @@ doty@my.laptop$ fwd some.server ``` `fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally. + +A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted. + Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port. Now, connections to that port locally will be forwarded to the remote server. @@ -21,9 +24,3 @@ If the port is something that might be interesting to a web browser, you can pre If something is going wrong, pressing `l` will toggle logs that might explain it. Press `q` to quit. - -## Future Improvements: - -- Clipboard integration: send something from the remote end of the pipe to the host's clipboard. (Sometimes you *really* want to copy some big buffer from the remote side and your terminal just can't make that work.) - -- Client heartbeats: I frequently wind up in a situation where the pipe is stalled: not broken but nothing is getting through. (This happens with my coder.com pipes all the time.) diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b0c87c2 --- /dev/null +++ b/build.rs @@ -0,0 +1,148 @@ +use std::io::Write; +use std::path::{absolute, Path, PathBuf}; + +/// Fetch the contents of the given file, and also tell cargo that we looked +/// in there. +fn file_contents>(path: P) -> String { + let path = + absolute(path.as_ref()).expect("Unable to make the path absolute"); + + let mut stdout = std::io::stdout(); + stdout + .write_all(b"cargo::rerun-if-changed=") + .expect("Unable to write stdout"); + stdout + .write_all(path.as_os_str().as_encoded_bytes()) + .expect("Unable to write path to stdout"); + stdout + .write_all(b"\n") + .expect("Unable to write newline to stdout"); + + std::fs::read_to_string(path).expect("Unable to read file") +} + +fn git_rel>(path: P) -> PathBuf { + let output = std::process::Command::new("git") + .arg("rev-parse") + .arg("--show-toplevel") + .output() + .expect("Error launching git rev-parse"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf8"); + eprintln!("`git rev-parse --show-toplevel` failed, stderr: {stderr}"); + panic!("`git rev-parse --show-toplevel` failed"); + } + + let mut root = PathBuf::from( + std::str::from_utf8(&output.stdout) + .expect("Output was not utf-8") + .trim(), + ); + root.push(path); + root +} + +/// Emit the current git commit. +fn emit_git_commit() { + // Fetch the current commit from the head. We do it this way instead of + // asking `git rev-parse` to do it for us because we want to reliably + // tell cargo which files it should monitor for changes. + let head = file_contents(git_rel(".git/HEAD")); + let rev = if let Some(r) = head.strip_prefix("ref: ") { + let mut ref_path = git_rel(".git/"); + ref_path.push(r.trim()); + file_contents(ref_path) + } else { + head + }; + + // But *now* we ask git rev-parse to make this into a short hash (a) to + // make sure we got it right and (b) because git knows how to quickly + // determine how much of a commit is required to be unique. We don't need + // to tell cargo anything here, no file that git consults will be + // mutable. + let output = std::process::Command::new("git") + .arg("rev-parse") + .arg("--short") + .arg(rev.trim()) + .output() + .expect("could not spawn `git` to get the hash"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf8"); + eprintln!("`git rev-parse --short HEAD` failed, stderr: {stderr}"); + panic!("`git rev-parse --short HEAD` failed"); + } + let rev = std::str::from_utf8(&output.stdout) + .expect("git did not output utf8") + .trim(); + + println!("cargo::rustc-env=REPO_REV={rev}"); +} + +fn emit_git_dirty() { + // Here is the way to see if anything is up with the repository: run `git + // status --porcelain=v1`. The status output in the v1 porcelain format + // has one line for every file that's modified in some way: staged, + // changed but unstaged, untracked, you name it. Files in the working + // tree that are up to date with the repository are not emitted. This is + // exactly what we want. + // + // (Yes, I want to track untracked files, because they can mess with the + // build too. The only good build is a clean build!) + let output = std::process::Command::new("git") + .arg("status") + .arg("-z") + .arg("--porcelain=v1") + .output() + .expect("could not spawn `git` to get repository status"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf8"); + eprintln!("`git status` failed, stderr: {stderr}"); + panic!("`git status` failed"); + } + let output = + std::str::from_utf8(&output.stdout).expect("git did not output utf8"); + + // Emit the repository status. + let dirty = if output.trim().is_empty() { + "" + } else { + " *dirty*" + }; + println!("cargo::rustc-env=REPO_DIRTY={dirty}"); + + // NOW: The output here has to do with *all* of the files in the git + // respository. (Because if nothing was modified, but then *becomes* + // modified, we need to rerun the script to notice the dirty bit.) + // `git-ls-files` is the way to do that. + let output = std::process::Command::new("git") + .arg("ls-files") + .arg("-z") + .arg("--cached") + .arg("--deleted") + .arg("--modified") + .arg("--others") + .arg("--exclude-standard") + .output() + .expect("could not spawn `git` to get repository status"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf-8"); + eprintln!("`git ls-files` failed, stderr: {stderr}"); + panic!("`git ls-files` failed"); + } + let output = + std::str::from_utf8(&output.stdout).expect("git did not output utf8"); + + for fname in output.split_terminator("\0") { + println!("cargo::rerun-if-changed={fname}"); + } +} + +fn main() { + emit_git_commit(); + emit_git_dirty(); +} diff --git a/doc/fwd.man.md b/doc/fwd.man.md new file mode 100644 index 0000000..5d5f819 --- /dev/null +++ b/doc/fwd.man.md @@ -0,0 +1,156 @@ +% fwd(1) +% John Doty +% August 2024 + +# NAME + +fwd - Automatically forward connections to remote machines + +# SYNOPSIS + +**fwd** [OPTIONS] SERVER + +**fwd** [OPTIONS] browse URL + +**fwd** [OPTIONS] clip FILE + +**fwd-browse** URL + +# DESCRIPTION + +**fwd** enumerates the listening ports the remote server and automatically listens for connections on the same ports on the local machine. +When **fwd** receives a connection on the local machine, it automatically forwards that connection to the remote machine. + +**-s**, **-\-sudo** +: Run the server side of fwd with `sudo`. +: This allows the client to forward ports that are open by processes being run under other accounts (e.g., docker containers being run as root), but requires sudo access on the server and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.) + +**-\-log-filter** **FILTER** +: Set remote server's log level. Default is `warn`. +: Supports all of Rust's env_logger filter syntax, e.g. `--log-filter=fwd::trace`. + +**-\-version** +: Print the version of fwd and exit. + +# INTERACTIVE COMMANDS + +Once **fwd** is connected, it displays an interactive list of the ports available on the remote server. + +- Ports that **fwd** is listening on are displayed in the default terminal color. +- Ports that **fwd** is aware of but which are disabled are displayed in dark gray. +- Ports that **fwd** has tried to listen on but which have failed are displayed in red. + Details on the error may be found in the log window. + Disabling and re-enabling the port will cause **fwd** to try again. + +The following commands are available while **fwd** is connected: + +**Esc, q, Ctrl-C** +: Exit **fwd**. + +**?, h** +: Display the help window. + +**Up, k** +: Select the previous port in the list. + +**Down, j** +: Select the next port in the list. + +**Enter** +: Attempt to browse to localhost on the specified port with the default browser. + +**a** +: Hide or show anonymous ports. +: (See "identifying ports" below for more information on anonymous ports.) + +**e** +: Enable or disable the selected port. + +**l** +: Show or hide the log window. + + +# IDENTIFYING PORTS + +**fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port. +It can identify ports in the following ways: + +*docker* +: **fwd** will attempt to find and connect to a docker engine on the remote machine. +: If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container. + +*procfs* +: On Linux, the listening ports are found by reading procfs and mapping them back to process command lines. +: **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine. + +(Earlier methods take precedence over later methods.) + +If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*. +Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration. + +# OPENING BROWSERS +**fwd** can be used to open URLs in the default browser on the local machine. +Run **fwd browse URL** on the remote server to open the `URL` in the default browser on the local machine. + +This only works if **fwd** is connected, and if the user running **fwd browse** is the same as the user that connected the **fwd** session. + +The **fwd-browse** program acts as a wrapper around **fwd browse**, to be used with configurations that can't handle a browser being a program with an argument. + +# CLIPBOARD +**fwd** can be used from the remote machine to place text on the clipboard of the local machine. +Run **fwd clip FILE** to copy the contents of the named file to the clipboard. +If **FILE** is **-**, this reads text from stdin instead. + +# CONFIGURATION +**fwd** can be configured with a configuration file. + +- On Windows, the config file will be in your roaming AppData folder. + (e.g., *c:\\Users\\Winifred\\AppData\\Roaming\\fwd\\config\\config.toml*) +- On MacOS, the config file will be in *$HOME/Library/Application Support/fwd/config.toml*. + (e.g., /Users/Margarie/Library/Application Support/fwd/config.toml) +- On XDG-ish systems (like Linux), the config file is in *~/.config/fwd/config.toml*. + (e.g., */home/lynette/.config/fwd/config.toml*) + +The following is an example of a *config.toml* file: + +``` +auto=true # should `fwd` should enable identified ports (default true) + +[servers.foo] # Server-specific settings for foo +auto=true # defaults to the global setting +ports=[1080, 1082] # ports that are always present + +[servers.bar.ports] # `ports` can also be a table with port numbers as keys +1080=true # the values can be booleans (for enabled)... +1081="My program" # or strings (for descriptions). + +[servers.bar.ports.1082] # port values can also be tables +enabled=true +description="A humble python" +``` + +Ports that are specified in the configuration file will always be present in the list of ports for a given server, even if no process is listening on that port. + +# TROUBLESHOOTING + +Connections are made via the **ssh** command. +Your **ssh** must: + +- Be on your path, so that **fwd** can find it to invoke it +- Be able to authenticate you to the remote server. + (Interactive authentication is fine.) +- Understand the **-D** command line option, to operate as a SOCKS5 server +- Be able to start the **fwd** command on the remote server + +A typical ssh invocation from **fwd** looks like: + +```bash +ssh -T -D XXXX me@server FWD_LOG=warning FWD_SEND_ANONYMOUS=1 fwd --server +``` + +**fwd** only enumerates ports that are listening on loopback addresses (e.g., 127.0.0.1) or on all addresses (e.g., 0.0.0.0). +If it cannot find a particular port, check to make sure that the process listening on that port is accessible via localhost. + +# SEE ALSO + +ssh(1) diff --git a/doc/screenshot-01.png b/doc/screenshot-01.png new file mode 100644 index 0000000..4f5315f Binary files /dev/null and b/doc/screenshot-01.png differ diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..855731d --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,1735 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix 0.38.34", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.34", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.6", +] + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "copypasta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix 0.38.34", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "env_filter", + "log", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "fwd" +version = "0.9.2" +dependencies = [ + "anyhow", + "bytes", + "copypasta", + "crossterm", + "env_logger", + "home", + "indoc", + "libc", + "log", + "open", + "procfs", + "rand", + "ratatui", + "thiserror", + "tokio", + "tokio-stream", + "toml", + "xdg", +] + +[[package]] +name = "fwd-fuzz" +version = "0.0.0" +dependencies = [ + "arbitrary", + "fwd", + "libfuzzer-sys", + "serde_json", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[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.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "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 = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "chrono", + "flate2", + "hex", + "lazy_static", + "rustix 0.36.17", +] + +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ratatui" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.36.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.1.4", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.207" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.207" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.34", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wayland-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.34", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +dependencies = [ + "bitflags 2.6.0", + "rustix 0.38.34", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.34", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.34", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..bc252d7 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "fwd-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +arbitrary = { version = "1.3.2", features = ["derive"] } +libfuzzer-sys = "0.4" +serde_json = "1.0.124" + +[dependencies.fwd] +path = ".." + +[[bin]] +name = "json_raw_input" +path = "fuzz_targets/json_raw_input.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "json_only_valid_serde" +path = "fuzz_targets/json_only_valid_serde.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/json_only_valid_serde.rs b/fuzz/fuzz_targets/json_only_valid_serde.rs new file mode 100644 index 0000000..567642f --- /dev/null +++ b/fuzz/fuzz_targets/json_only_valid_serde.rs @@ -0,0 +1,77 @@ +#![no_main] + +use arbitrary::{Arbitrary, Error, Unstructured}; +use libfuzzer_sys::fuzz_target; +use std::collections::HashMap; + +extern crate fwd; +use fwd::server::refresh::docker::JsonValue; + +/// InputNumber is a JSON number, i.e., a finite 64-bit floating point value +/// that is not NaN. We need to define our own little wrapper here so that we +/// can convince Arbitrary to only make finite f64s. +/// +/// Ideally we would actually wrap serde_json::Number but there are rules +/// about mixing 3rd party traits with 3rd party types. +#[derive(Debug, PartialEq)] +struct InputNumber(f64); + +impl<'a> Arbitrary<'a> for InputNumber { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let value = f64::arbitrary(u)?; + if value.is_finite() { + Ok(InputNumber(value)) + } else { + Err(Error::IncorrectFormat) // REJECT + } + } + + #[inline] + fn size_hint(depth: usize) -> (usize, Option) { + f64::size_hint(depth) + } +} + +/// TestInput is basically serde_json::Value, except (a) it has a HashMap and +/// not serde_json's special `Map` structure, and (b) it has `InputNumber` +/// instead of `json_serde::Number` for reasons described above. +#[derive(Debug, PartialEq, Arbitrary)] +enum TestInput { + Null, + Bool(bool), + Number(InputNumber), + String(String), + Object(HashMap), + Array(Vec), +} + +fn convert(value: &TestInput) -> serde_json::Value { + match value { + TestInput::Null => serde_json::Value::Null, + TestInput::Bool(b) => serde_json::Value::Bool(*b), + TestInput::Number(n) => serde_json::Value::Number( + serde_json::Number::from_f64(n.0).expect("Unable to make an f64"), + ), + TestInput::String(s) => serde_json::Value::String(s.clone()), + TestInput::Object(o) => { + let mut out = serde_json::map::Map::new(); + for (k, v) in o.into_iter() { + out.insert(k.clone(), convert(v)); + } + serde_json::Value::Object(out) + } + TestInput::Array(v) => { + serde_json::Value::Array(v.into_iter().map(convert).collect()) + } + } +} + +fuzz_target!(|data: TestInput| { + // Convert the arbitrary TestInput into an arbitrary serde_json::Value, + // then use serde_json to write out arbitrary JSON. + let converted = convert(&data).to_string(); + + // Parse the JSON that serde_json produced. This fuzz test should ensure + // that we can parse anything that serde_json can produce. + let _ = JsonValue::parse(converted.as_bytes()); +}); diff --git a/fuzz/fuzz_targets/json_raw_input.rs b/fuzz/fuzz_targets/json_raw_input.rs new file mode 100644 index 0000000..3178e8b --- /dev/null +++ b/fuzz/fuzz_targets/json_raw_input.rs @@ -0,0 +1,10 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +extern crate fwd; +use fwd::server::refresh::docker::JsonValue; + +fuzz_target!(|data: &[u8]| { + let _ = JsonValue::parse(data); +}); diff --git a/release.py b/release.py new file mode 100644 index 0000000..0fa912b --- /dev/null +++ b/release.py @@ -0,0 +1,161 @@ +"""A script to automate building and uploading a release archive. + +This is in python instead of bash because I abhor bash. Even though it's a +little nicer for running commands, it's worse at everything else. +""" + +import dataclasses +import enum +import os +import os.path +import pathlib +import shutil +import subprocess + +RELEASE_TAG = os.getenv("RELEASE_TAG") + +BUILD = os.getenv("BUILD") +if BUILD is None: + raise Exception("you *must* set the BUILD environment variable") + + +class Archive(enum.Enum): + TARBALL = 1 + ZIP = 2 + DEB = 3 + + +@dataclasses.dataclass +class BuildSettings: + target: str # The rust target to build for + test: bool = True # Whether or not to run tests + man_page: bool = True # Whether or not to generate a man page + strip: bool = True # Whether or not to strip binaries + archive: Archive = Archive.TARBALL # Archive type + ext: str = "" # The file extension of the binary + + +print(f"doing release: {BUILD}") +build = { + "linux": BuildSettings( + target="x86_64-unknown-linux-musl", + ), + "debian": BuildSettings( + target="x86_64-unknown-linux-musl", + test=False, + archive=Archive.DEB, + ), + "macos": BuildSettings( + target="x86_64-apple-darwin", + ), + "arm-macos": BuildSettings( + target="aarch64-apple-darwin", + ), + "windows": BuildSettings( + target="x86_64-pc-windows-msvc", + strip=False, + man_page=False, + archive=Archive.ZIP, + ext=".exe", + ), +}[BUILD] + +print(f"settings: {build}") + + +target_dir = pathlib.Path("target") / build.target / "release" +bins = [(target_dir / bin).with_suffix(build.ext) for bin in ["fwd", "fwd-browse"]] + + +def build_and_test(staging: pathlib.Path): + # Tools + subprocess.run( + ["rustup", "target", "add", build.target], + check=True, + ) + + # Test...? + if build.test: + subprocess.run( + ["cargo", "test", "--verbose", "--release", "--target", build.target], + check=True, + ) + + # Build + subprocess.run( + ["cargo", "build", "--verbose", "--release", "--target", build.target], + check=True, + ) + + # Strip + if build.strip: + for bin in bins: + subprocess.run(["strip", bin], check=True) + + # Copy + for bin in bins: + shutil.copyfile(bin, os.path.join(staging, os.path.basename(bin))) + + +def build_docs(staging: pathlib.Path): + shutil.copyfile("README.md", staging / "README.md") + if build.man_page: + print("Creating man page...") + proc = subprocess.run( + ["pandoc", "-s", "-tman", os.path.join("doc", "fwd.man.md")], + check=True, + capture_output=True, + encoding="utf8", + ) + contents = proc.stdout + with open(staging / "fwd.1", "w", encoding="utf-8") as f: + f.write(contents) + + +def build_archive(staging: pathlib.Path) -> pathlib.Path: + print("Creating archive...") + if build.archive == Archive.ZIP: + archive = pathlib.Path(f"{staging}.zip") + subprocess.run(["7z", "a", archive, f"{staging}"], check=True) + + elif build.archive == Archive.DEB: + subprocess.run(["cargo", "install", "cargo-deb"], check=True) + shutil.copyfile(staging / "fwd.1", target_dir / "fwd.1") + subprocess.run(["cargo", "deb", "--target", build.target], check=True) + + # Knowing the deb path means knowing the target version but I don't + # actually have the version here. (Or, like, I have the release tag + # but not in testing.) So just find the hopefully singular .deb that + # we made. + deb_path = pathlib.Path("target") / build.target / "debian" + archives = list(deb_path.glob("*.deb")) + assert len(archives) == 1 + archive = archives[0] + + else: + assert build.archive == Archive.TARBALL + archive = pathlib.Path(f"{staging}.tar.gz") + subprocess.run(["tar", "czf", archive, f"{staging}"], check=True) + + return archive + + +staging = pathlib.Path(f"fwd-{build.target}") +os.makedirs(staging, exist_ok=True) + +build_and_test(staging) +build_docs(staging) +archive = build_archive(staging) + +shutil.rmtree(staging) + +assert archive.exists() +if RELEASE_TAG is None: + print(f"Not releasing {archive} to github, RELEASE_TAG is none.") +else: + print(f"Uploading {archive} to github release {RELEASE_TAG}...") + subprocess.run( + ["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"], + check=True, + ) + os.unlink(archive) diff --git a/resources/json/README.md b/resources/json/README.md new file mode 100644 index 0000000..533092f --- /dev/null +++ b/resources/json/README.md @@ -0,0 +1,14 @@ +# Test JSON + +This directory contains test JSON files from https://github.com/nst/JSONTestSuite as of commit 984defc. + +It only has the positive and questionable JSON inputs, as our JSON parser is extremely forgiving, by design. + +## Filtered tests + +Some of the questionable tests have been removed: + +- `i_structure_UTF-8_BOM_empty_object.json` removed because we don't handle BOMs. +- `i_string_utf16LE_no_BOM.json` removed because we don't speak UTF16. +- `i_string_utf16BE_no_BOM.json` removed because we don't speak UTF16. +- `i_string_UTF-16LE_with_BOM.json` removed because we don't speak UTF16. diff --git a/resources/json/i_number_double_huge_neg_exp.json b/resources/json/i_number_double_huge_neg_exp.json new file mode 100644 index 0000000..ae4c7b7 --- /dev/null +++ b/resources/json/i_number_double_huge_neg_exp.json @@ -0,0 +1 @@ +[123.456e-789] \ No newline at end of file diff --git a/resources/json/i_number_huge_exp.json b/resources/json/i_number_huge_exp.json new file mode 100644 index 0000000..9b5efa2 --- /dev/null +++ b/resources/json/i_number_huge_exp.json @@ -0,0 +1 @@ +[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006] \ No newline at end of file diff --git a/resources/json/i_number_neg_int_huge_exp.json b/resources/json/i_number_neg_int_huge_exp.json new file mode 100755 index 0000000..3abd58a --- /dev/null +++ b/resources/json/i_number_neg_int_huge_exp.json @@ -0,0 +1 @@ +[-1e+9999] \ No newline at end of file diff --git a/resources/json/i_number_pos_double_huge_exp.json b/resources/json/i_number_pos_double_huge_exp.json new file mode 100755 index 0000000..e10a7eb --- /dev/null +++ b/resources/json/i_number_pos_double_huge_exp.json @@ -0,0 +1 @@ +[1.5e+9999] \ No newline at end of file diff --git a/resources/json/i_number_real_neg_overflow.json b/resources/json/i_number_real_neg_overflow.json new file mode 100644 index 0000000..3d628a9 --- /dev/null +++ b/resources/json/i_number_real_neg_overflow.json @@ -0,0 +1 @@ +[-123123e100000] \ No newline at end of file diff --git a/resources/json/i_number_real_pos_overflow.json b/resources/json/i_number_real_pos_overflow.json new file mode 100644 index 0000000..54d7d3d --- /dev/null +++ b/resources/json/i_number_real_pos_overflow.json @@ -0,0 +1 @@ +[123123e100000] \ No newline at end of file diff --git a/resources/json/i_number_real_underflow.json b/resources/json/i_number_real_underflow.json new file mode 100644 index 0000000..c5236eb --- /dev/null +++ b/resources/json/i_number_real_underflow.json @@ -0,0 +1 @@ +[123e-10000000] \ No newline at end of file diff --git a/resources/json/i_number_too_big_neg_int.json b/resources/json/i_number_too_big_neg_int.json new file mode 100644 index 0000000..dfa3846 --- /dev/null +++ b/resources/json/i_number_too_big_neg_int.json @@ -0,0 +1 @@ +[-123123123123123123123123123123] \ No newline at end of file diff --git a/resources/json/i_number_too_big_pos_int.json b/resources/json/i_number_too_big_pos_int.json new file mode 100644 index 0000000..338a8c3 --- /dev/null +++ b/resources/json/i_number_too_big_pos_int.json @@ -0,0 +1 @@ +[100000000000000000000] \ No newline at end of file diff --git a/resources/json/i_number_very_big_negative_int.json b/resources/json/i_number_very_big_negative_int.json new file mode 100755 index 0000000..e2d9738 --- /dev/null +++ b/resources/json/i_number_very_big_negative_int.json @@ -0,0 +1 @@ +[-237462374673276894279832749832423479823246327846] \ No newline at end of file diff --git a/resources/json/i_object_key_lone_2nd_surrogate.json b/resources/json/i_object_key_lone_2nd_surrogate.json new file mode 100644 index 0000000..5be7eba --- /dev/null +++ b/resources/json/i_object_key_lone_2nd_surrogate.json @@ -0,0 +1 @@ +{"\uDFAA":0} \ No newline at end of file diff --git a/resources/json/i_string_1st_surrogate_but_2nd_missing.json b/resources/json/i_string_1st_surrogate_but_2nd_missing.json new file mode 100644 index 0000000..3b9e37c --- /dev/null +++ b/resources/json/i_string_1st_surrogate_but_2nd_missing.json @@ -0,0 +1 @@ +["\uDADA"] \ No newline at end of file diff --git a/resources/json/i_string_1st_valid_surrogate_2nd_invalid.json b/resources/json/i_string_1st_valid_surrogate_2nd_invalid.json new file mode 100644 index 0000000..4875928 --- /dev/null +++ b/resources/json/i_string_1st_valid_surrogate_2nd_invalid.json @@ -0,0 +1 @@ +["\uD888\u1234"] \ No newline at end of file diff --git a/resources/json/i_string_UTF-8_invalid_sequence.json b/resources/json/i_string_UTF-8_invalid_sequence.json new file mode 100755 index 0000000..e2a968a --- /dev/null +++ b/resources/json/i_string_UTF-8_invalid_sequence.json @@ -0,0 +1 @@ +["日шú"] \ No newline at end of file diff --git a/resources/json/i_string_UTF8_surrogate_U+D800.json b/resources/json/i_string_UTF8_surrogate_U+D800.json new file mode 100644 index 0000000..916bff9 --- /dev/null +++ b/resources/json/i_string_UTF8_surrogate_U+D800.json @@ -0,0 +1 @@ +["í €"] \ No newline at end of file diff --git a/resources/json/i_string_incomplete_surrogate_and_escape_valid.json b/resources/json/i_string_incomplete_surrogate_and_escape_valid.json new file mode 100755 index 0000000..3cb11d2 --- /dev/null +++ b/resources/json/i_string_incomplete_surrogate_and_escape_valid.json @@ -0,0 +1 @@ +["\uD800\n"] \ No newline at end of file diff --git a/resources/json/i_string_incomplete_surrogate_pair.json b/resources/json/i_string_incomplete_surrogate_pair.json new file mode 100755 index 0000000..38ec23b --- /dev/null +++ b/resources/json/i_string_incomplete_surrogate_pair.json @@ -0,0 +1 @@ +["\uDd1ea"] \ No newline at end of file diff --git a/resources/json/i_string_incomplete_surrogates_escape_valid.json b/resources/json/i_string_incomplete_surrogates_escape_valid.json new file mode 100755 index 0000000..c9cd6f6 --- /dev/null +++ b/resources/json/i_string_incomplete_surrogates_escape_valid.json @@ -0,0 +1 @@ +["\uD800\uD800\n"] \ No newline at end of file diff --git a/resources/json/i_string_invalid_lonely_surrogate.json b/resources/json/i_string_invalid_lonely_surrogate.json new file mode 100755 index 0000000..3abbd8d --- /dev/null +++ b/resources/json/i_string_invalid_lonely_surrogate.json @@ -0,0 +1 @@ +["\ud800"] \ No newline at end of file diff --git a/resources/json/i_string_invalid_surrogate.json b/resources/json/i_string_invalid_surrogate.json new file mode 100755 index 0000000..ffddc04 --- /dev/null +++ b/resources/json/i_string_invalid_surrogate.json @@ -0,0 +1 @@ +["\ud800abc"] \ No newline at end of file diff --git a/resources/json/i_string_invalid_utf-8.json b/resources/json/i_string_invalid_utf-8.json new file mode 100644 index 0000000..8e45a7e --- /dev/null +++ b/resources/json/i_string_invalid_utf-8.json @@ -0,0 +1 @@ +["ÿ"] \ No newline at end of file diff --git a/resources/json/i_string_inverted_surrogates_U+1D11E.json b/resources/json/i_string_inverted_surrogates_U+1D11E.json new file mode 100755 index 0000000..0d5456c --- /dev/null +++ b/resources/json/i_string_inverted_surrogates_U+1D11E.json @@ -0,0 +1 @@ +["\uDd1e\uD834"] \ No newline at end of file diff --git a/resources/json/i_string_iso_latin_1.json b/resources/json/i_string_iso_latin_1.json new file mode 100644 index 0000000..9389c98 --- /dev/null +++ b/resources/json/i_string_iso_latin_1.json @@ -0,0 +1 @@ +["é"] \ No newline at end of file diff --git a/resources/json/i_string_lone_second_surrogate.json b/resources/json/i_string_lone_second_surrogate.json new file mode 100644 index 0000000..1dbd397 --- /dev/null +++ b/resources/json/i_string_lone_second_surrogate.json @@ -0,0 +1 @@ +["\uDFAA"] \ No newline at end of file diff --git a/resources/json/i_string_lone_utf8_continuation_byte.json b/resources/json/i_string_lone_utf8_continuation_byte.json new file mode 100644 index 0000000..729337c --- /dev/null +++ b/resources/json/i_string_lone_utf8_continuation_byte.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/resources/json/i_string_not_in_unicode_range.json b/resources/json/i_string_not_in_unicode_range.json new file mode 100644 index 0000000..df90a29 --- /dev/null +++ b/resources/json/i_string_not_in_unicode_range.json @@ -0,0 +1 @@ +["ô¿¿¿"] \ No newline at end of file diff --git a/resources/json/i_string_overlong_sequence_2_bytes.json b/resources/json/i_string_overlong_sequence_2_bytes.json new file mode 100644 index 0000000..c8cee5e --- /dev/null +++ b/resources/json/i_string_overlong_sequence_2_bytes.json @@ -0,0 +1 @@ +["À¯"] \ No newline at end of file diff --git a/resources/json/i_string_overlong_sequence_6_bytes.json b/resources/json/i_string_overlong_sequence_6_bytes.json new file mode 100755 index 0000000..9a91da7 --- /dev/null +++ b/resources/json/i_string_overlong_sequence_6_bytes.json @@ -0,0 +1 @@ +["üƒ¿¿¿¿"] \ No newline at end of file diff --git a/resources/json/i_string_overlong_sequence_6_bytes_null.json b/resources/json/i_string_overlong_sequence_6_bytes_null.json new file mode 100755 index 0000000..d24fffd --- /dev/null +++ b/resources/json/i_string_overlong_sequence_6_bytes_null.json @@ -0,0 +1 @@ +["ü€€€€€"] \ No newline at end of file diff --git a/resources/json/i_string_truncated-utf-8.json b/resources/json/i_string_truncated-utf-8.json new file mode 100644 index 0000000..63c7777 --- /dev/null +++ b/resources/json/i_string_truncated-utf-8.json @@ -0,0 +1 @@ +["àÿ"] \ No newline at end of file diff --git a/resources/json/i_structure_500_nested_arrays.json b/resources/json/i_structure_500_nested_arrays.json new file mode 100644 index 0000000..7118405 --- /dev/null +++ b/resources/json/i_structure_500_nested_arrays.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] \ No newline at end of file diff --git a/resources/json/y_array_arraysWithSpaces.json b/resources/json/y_array_arraysWithSpaces.json new file mode 100755 index 0000000..5822907 --- /dev/null +++ b/resources/json/y_array_arraysWithSpaces.json @@ -0,0 +1 @@ +[[] ] \ No newline at end of file diff --git a/resources/json/y_array_empty-string.json b/resources/json/y_array_empty-string.json new file mode 100644 index 0000000..93b6be2 --- /dev/null +++ b/resources/json/y_array_empty-string.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/resources/json/y_array_empty.json b/resources/json/y_array_empty.json new file mode 100755 index 0000000..0637a08 --- /dev/null +++ b/resources/json/y_array_empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/resources/json/y_array_ending_with_newline.json b/resources/json/y_array_ending_with_newline.json new file mode 100755 index 0000000..eac5f7b --- /dev/null +++ b/resources/json/y_array_ending_with_newline.json @@ -0,0 +1 @@ +["a"] \ No newline at end of file diff --git a/resources/json/y_array_false.json b/resources/json/y_array_false.json new file mode 100644 index 0000000..67b2f07 --- /dev/null +++ b/resources/json/y_array_false.json @@ -0,0 +1 @@ +[false] \ No newline at end of file diff --git a/resources/json/y_array_heterogeneous.json b/resources/json/y_array_heterogeneous.json new file mode 100755 index 0000000..d3c1e26 --- /dev/null +++ b/resources/json/y_array_heterogeneous.json @@ -0,0 +1 @@ +[null, 1, "1", {}] \ No newline at end of file diff --git a/resources/json/y_array_null.json b/resources/json/y_array_null.json new file mode 100644 index 0000000..500db4a --- /dev/null +++ b/resources/json/y_array_null.json @@ -0,0 +1 @@ +[null] \ No newline at end of file diff --git a/resources/json/y_array_with_1_and_newline.json b/resources/json/y_array_with_1_and_newline.json new file mode 100644 index 0000000..9948255 --- /dev/null +++ b/resources/json/y_array_with_1_and_newline.json @@ -0,0 +1,2 @@ +[1 +] \ No newline at end of file diff --git a/resources/json/y_array_with_leading_space.json b/resources/json/y_array_with_leading_space.json new file mode 100755 index 0000000..18bfe64 --- /dev/null +++ b/resources/json/y_array_with_leading_space.json @@ -0,0 +1 @@ + [1] \ No newline at end of file diff --git a/resources/json/y_array_with_several_null.json b/resources/json/y_array_with_several_null.json new file mode 100755 index 0000000..99f6c5d --- /dev/null +++ b/resources/json/y_array_with_several_null.json @@ -0,0 +1 @@ +[1,null,null,null,2] \ No newline at end of file diff --git a/resources/json/y_array_with_trailing_space.json b/resources/json/y_array_with_trailing_space.json new file mode 100755 index 0000000..de9e7a9 --- /dev/null +++ b/resources/json/y_array_with_trailing_space.json @@ -0,0 +1 @@ +[2] \ No newline at end of file diff --git a/resources/json/y_number.json b/resources/json/y_number.json new file mode 100644 index 0000000..e5f5cc3 --- /dev/null +++ b/resources/json/y_number.json @@ -0,0 +1 @@ +[123e65] \ No newline at end of file diff --git a/resources/json/y_number_0e+1.json b/resources/json/y_number_0e+1.json new file mode 100755 index 0000000..d1d3967 --- /dev/null +++ b/resources/json/y_number_0e+1.json @@ -0,0 +1 @@ +[0e+1] \ No newline at end of file diff --git a/resources/json/y_number_0e1.json b/resources/json/y_number_0e1.json new file mode 100755 index 0000000..3283a79 --- /dev/null +++ b/resources/json/y_number_0e1.json @@ -0,0 +1 @@ +[0e1] \ No newline at end of file diff --git a/resources/json/y_number_after_space.json b/resources/json/y_number_after_space.json new file mode 100644 index 0000000..623570d --- /dev/null +++ b/resources/json/y_number_after_space.json @@ -0,0 +1 @@ +[ 4] \ No newline at end of file diff --git a/resources/json/y_number_double_close_to_zero.json b/resources/json/y_number_double_close_to_zero.json new file mode 100755 index 0000000..96555ff --- /dev/null +++ b/resources/json/y_number_double_close_to_zero.json @@ -0,0 +1 @@ +[-0.000000000000000000000000000000000000000000000000000000000000000000000000000001] diff --git a/resources/json/y_number_int_with_exp.json b/resources/json/y_number_int_with_exp.json new file mode 100755 index 0000000..a4ca9e7 --- /dev/null +++ b/resources/json/y_number_int_with_exp.json @@ -0,0 +1 @@ +[20e1] \ No newline at end of file diff --git a/resources/json/y_number_minus_zero.json b/resources/json/y_number_minus_zero.json new file mode 100755 index 0000000..37af131 --- /dev/null +++ b/resources/json/y_number_minus_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/resources/json/y_number_negative_int.json b/resources/json/y_number_negative_int.json new file mode 100644 index 0000000..8e30f8b --- /dev/null +++ b/resources/json/y_number_negative_int.json @@ -0,0 +1 @@ +[-123] \ No newline at end of file diff --git a/resources/json/y_number_negative_one.json b/resources/json/y_number_negative_one.json new file mode 100644 index 0000000..99d21a2 --- /dev/null +++ b/resources/json/y_number_negative_one.json @@ -0,0 +1 @@ +[-1] \ No newline at end of file diff --git a/resources/json/y_number_negative_zero.json b/resources/json/y_number_negative_zero.json new file mode 100644 index 0000000..37af131 --- /dev/null +++ b/resources/json/y_number_negative_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/resources/json/y_number_real_capital_e.json b/resources/json/y_number_real_capital_e.json new file mode 100644 index 0000000..6edbdfc --- /dev/null +++ b/resources/json/y_number_real_capital_e.json @@ -0,0 +1 @@ +[1E22] \ No newline at end of file diff --git a/resources/json/y_number_real_capital_e_neg_exp.json b/resources/json/y_number_real_capital_e_neg_exp.json new file mode 100644 index 0000000..0a01bd3 --- /dev/null +++ b/resources/json/y_number_real_capital_e_neg_exp.json @@ -0,0 +1 @@ +[1E-2] \ No newline at end of file diff --git a/resources/json/y_number_real_capital_e_pos_exp.json b/resources/json/y_number_real_capital_e_pos_exp.json new file mode 100644 index 0000000..5a8fc09 --- /dev/null +++ b/resources/json/y_number_real_capital_e_pos_exp.json @@ -0,0 +1 @@ +[1E+2] \ No newline at end of file diff --git a/resources/json/y_number_real_exponent.json b/resources/json/y_number_real_exponent.json new file mode 100644 index 0000000..da2522d --- /dev/null +++ b/resources/json/y_number_real_exponent.json @@ -0,0 +1 @@ +[123e45] \ No newline at end of file diff --git a/resources/json/y_number_real_fraction_exponent.json b/resources/json/y_number_real_fraction_exponent.json new file mode 100644 index 0000000..3944a7a --- /dev/null +++ b/resources/json/y_number_real_fraction_exponent.json @@ -0,0 +1 @@ +[123.456e78] \ No newline at end of file diff --git a/resources/json/y_number_real_neg_exp.json b/resources/json/y_number_real_neg_exp.json new file mode 100644 index 0000000..ca40d3c --- /dev/null +++ b/resources/json/y_number_real_neg_exp.json @@ -0,0 +1 @@ +[1e-2] \ No newline at end of file diff --git a/resources/json/y_number_real_pos_exponent.json b/resources/json/y_number_real_pos_exponent.json new file mode 100644 index 0000000..343601d --- /dev/null +++ b/resources/json/y_number_real_pos_exponent.json @@ -0,0 +1 @@ +[1e+2] \ No newline at end of file diff --git a/resources/json/y_number_simple_int.json b/resources/json/y_number_simple_int.json new file mode 100644 index 0000000..e47f69a --- /dev/null +++ b/resources/json/y_number_simple_int.json @@ -0,0 +1 @@ +[123] \ No newline at end of file diff --git a/resources/json/y_number_simple_real.json b/resources/json/y_number_simple_real.json new file mode 100644 index 0000000..b02878e --- /dev/null +++ b/resources/json/y_number_simple_real.json @@ -0,0 +1 @@ +[123.456789] \ No newline at end of file diff --git a/resources/json/y_object.json b/resources/json/y_object.json new file mode 100755 index 0000000..78262ed --- /dev/null +++ b/resources/json/y_object.json @@ -0,0 +1 @@ +{"asd":"sdf", "dfg":"fgh"} \ No newline at end of file diff --git a/resources/json/y_object_basic.json b/resources/json/y_object_basic.json new file mode 100755 index 0000000..646bbe7 --- /dev/null +++ b/resources/json/y_object_basic.json @@ -0,0 +1 @@ +{"asd":"sdf"} \ No newline at end of file diff --git a/resources/json/y_object_duplicated_key.json b/resources/json/y_object_duplicated_key.json new file mode 100755 index 0000000..bbc2e1c --- /dev/null +++ b/resources/json/y_object_duplicated_key.json @@ -0,0 +1 @@ +{"a":"b","a":"c"} \ No newline at end of file diff --git a/resources/json/y_object_duplicated_key_and_value.json b/resources/json/y_object_duplicated_key_and_value.json new file mode 100755 index 0000000..211581c --- /dev/null +++ b/resources/json/y_object_duplicated_key_and_value.json @@ -0,0 +1 @@ +{"a":"b","a":"b"} \ No newline at end of file diff --git a/resources/json/y_object_empty.json b/resources/json/y_object_empty.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/resources/json/y_object_empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/resources/json/y_object_empty_key.json b/resources/json/y_object_empty_key.json new file mode 100755 index 0000000..c0013d3 --- /dev/null +++ b/resources/json/y_object_empty_key.json @@ -0,0 +1 @@ +{"":0} \ No newline at end of file diff --git a/resources/json/y_object_escaped_null_in_key.json b/resources/json/y_object_escaped_null_in_key.json new file mode 100644 index 0000000..593f0f6 --- /dev/null +++ b/resources/json/y_object_escaped_null_in_key.json @@ -0,0 +1 @@ +{"foo\u0000bar": 42} \ No newline at end of file diff --git a/resources/json/y_object_extreme_numbers.json b/resources/json/y_object_extreme_numbers.json new file mode 100644 index 0000000..a0d3531 --- /dev/null +++ b/resources/json/y_object_extreme_numbers.json @@ -0,0 +1 @@ +{ "min": -1.0e+28, "max": 1.0e+28 } \ No newline at end of file diff --git a/resources/json/y_object_long_strings.json b/resources/json/y_object_long_strings.json new file mode 100644 index 0000000..bdc4a08 --- /dev/null +++ b/resources/json/y_object_long_strings.json @@ -0,0 +1 @@ +{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} \ No newline at end of file diff --git a/resources/json/y_object_simple.json b/resources/json/y_object_simple.json new file mode 100644 index 0000000..dacac91 --- /dev/null +++ b/resources/json/y_object_simple.json @@ -0,0 +1 @@ +{"a":[]} \ No newline at end of file diff --git a/resources/json/y_object_string_unicode.json b/resources/json/y_object_string_unicode.json new file mode 100644 index 0000000..8effdb2 --- /dev/null +++ b/resources/json/y_object_string_unicode.json @@ -0,0 +1 @@ +{"title":"\u041f\u043e\u043b\u0442\u043e\u0440\u0430 \u0417\u0435\u043c\u043b\u0435\u043a\u043e\u043f\u0430" } \ No newline at end of file diff --git a/resources/json/y_object_with_newlines.json b/resources/json/y_object_with_newlines.json new file mode 100644 index 0000000..246ec6b --- /dev/null +++ b/resources/json/y_object_with_newlines.json @@ -0,0 +1,3 @@ +{ +"a": "b" +} \ No newline at end of file diff --git a/resources/json/y_string_1_2_3_bytes_UTF-8_sequences.json b/resources/json/y_string_1_2_3_bytes_UTF-8_sequences.json new file mode 100755 index 0000000..9967dde --- /dev/null +++ b/resources/json/y_string_1_2_3_bytes_UTF-8_sequences.json @@ -0,0 +1 @@ +["\u0060\u012a\u12AB"] \ No newline at end of file diff --git a/resources/json/y_string_accepted_surrogate_pair.json b/resources/json/y_string_accepted_surrogate_pair.json new file mode 100755 index 0000000..996875c --- /dev/null +++ b/resources/json/y_string_accepted_surrogate_pair.json @@ -0,0 +1 @@ +["\uD801\udc37"] \ No newline at end of file diff --git a/resources/json/y_string_accepted_surrogate_pairs.json b/resources/json/y_string_accepted_surrogate_pairs.json new file mode 100755 index 0000000..3401021 --- /dev/null +++ b/resources/json/y_string_accepted_surrogate_pairs.json @@ -0,0 +1 @@ +["\ud83d\ude39\ud83d\udc8d"] \ No newline at end of file diff --git a/resources/json/y_string_allowed_escapes.json b/resources/json/y_string_allowed_escapes.json new file mode 100644 index 0000000..7f49553 --- /dev/null +++ b/resources/json/y_string_allowed_escapes.json @@ -0,0 +1 @@ +["\"\\\/\b\f\n\r\t"] \ No newline at end of file diff --git a/resources/json/y_string_backslash_and_u_escaped_zero.json b/resources/json/y_string_backslash_and_u_escaped_zero.json new file mode 100755 index 0000000..d4439ed --- /dev/null +++ b/resources/json/y_string_backslash_and_u_escaped_zero.json @@ -0,0 +1 @@ +["\\u0000"] \ No newline at end of file diff --git a/resources/json/y_string_backslash_doublequotes.json b/resources/json/y_string_backslash_doublequotes.json new file mode 100644 index 0000000..ae03243 --- /dev/null +++ b/resources/json/y_string_backslash_doublequotes.json @@ -0,0 +1 @@ +["\""] \ No newline at end of file diff --git a/resources/json/y_string_comments.json b/resources/json/y_string_comments.json new file mode 100644 index 0000000..2260c20 --- /dev/null +++ b/resources/json/y_string_comments.json @@ -0,0 +1 @@ +["a/*b*/c/*d//e"] \ No newline at end of file diff --git a/resources/json/y_string_double_escape_a.json b/resources/json/y_string_double_escape_a.json new file mode 100644 index 0000000..6715d6f --- /dev/null +++ b/resources/json/y_string_double_escape_a.json @@ -0,0 +1 @@ +["\\a"] \ No newline at end of file diff --git a/resources/json/y_string_double_escape_n.json b/resources/json/y_string_double_escape_n.json new file mode 100644 index 0000000..44ca56c --- /dev/null +++ b/resources/json/y_string_double_escape_n.json @@ -0,0 +1 @@ +["\\n"] \ No newline at end of file diff --git a/resources/json/y_string_escaped_control_character.json b/resources/json/y_string_escaped_control_character.json new file mode 100644 index 0000000..5b014a9 --- /dev/null +++ b/resources/json/y_string_escaped_control_character.json @@ -0,0 +1 @@ +["\u0012"] \ No newline at end of file diff --git a/resources/json/y_string_escaped_noncharacter.json b/resources/json/y_string_escaped_noncharacter.json new file mode 100755 index 0000000..2ff52e2 --- /dev/null +++ b/resources/json/y_string_escaped_noncharacter.json @@ -0,0 +1 @@ +["\uFFFF"] \ No newline at end of file diff --git a/resources/json/y_string_in_array.json b/resources/json/y_string_in_array.json new file mode 100755 index 0000000..21d7ae4 --- /dev/null +++ b/resources/json/y_string_in_array.json @@ -0,0 +1 @@ +["asd"] \ No newline at end of file diff --git a/resources/json/y_string_in_array_with_leading_space.json b/resources/json/y_string_in_array_with_leading_space.json new file mode 100755 index 0000000..9e1887c --- /dev/null +++ b/resources/json/y_string_in_array_with_leading_space.json @@ -0,0 +1 @@ +[ "asd"] \ No newline at end of file diff --git a/resources/json/y_string_last_surrogates_1_and_2.json b/resources/json/y_string_last_surrogates_1_and_2.json new file mode 100644 index 0000000..3919cef --- /dev/null +++ b/resources/json/y_string_last_surrogates_1_and_2.json @@ -0,0 +1 @@ +["\uDBFF\uDFFF"] \ No newline at end of file diff --git a/resources/json/y_string_nbsp_uescaped.json b/resources/json/y_string_nbsp_uescaped.json new file mode 100644 index 0000000..2085ab1 --- /dev/null +++ b/resources/json/y_string_nbsp_uescaped.json @@ -0,0 +1 @@ +["new\u00A0line"] \ No newline at end of file diff --git a/resources/json/y_string_nonCharacterInUTF-8_U+10FFFF.json b/resources/json/y_string_nonCharacterInUTF-8_U+10FFFF.json new file mode 100755 index 0000000..059e4d9 --- /dev/null +++ b/resources/json/y_string_nonCharacterInUTF-8_U+10FFFF.json @@ -0,0 +1 @@ +["ô¿¿"] \ No newline at end of file diff --git a/resources/json/y_string_nonCharacterInUTF-8_U+FFFF.json b/resources/json/y_string_nonCharacterInUTF-8_U+FFFF.json new file mode 100755 index 0000000..4c913bd --- /dev/null +++ b/resources/json/y_string_nonCharacterInUTF-8_U+FFFF.json @@ -0,0 +1 @@ +["ï¿¿"] \ No newline at end of file diff --git a/resources/json/y_string_null_escape.json b/resources/json/y_string_null_escape.json new file mode 100644 index 0000000..c1ad844 --- /dev/null +++ b/resources/json/y_string_null_escape.json @@ -0,0 +1 @@ +["\u0000"] \ No newline at end of file diff --git a/resources/json/y_string_one-byte-utf-8.json b/resources/json/y_string_one-byte-utf-8.json new file mode 100644 index 0000000..1571859 --- /dev/null +++ b/resources/json/y_string_one-byte-utf-8.json @@ -0,0 +1 @@ +["\u002c"] \ No newline at end of file diff --git a/resources/json/y_string_pi.json b/resources/json/y_string_pi.json new file mode 100644 index 0000000..9df11ae --- /dev/null +++ b/resources/json/y_string_pi.json @@ -0,0 +1 @@ +["Ï€"] \ No newline at end of file diff --git a/resources/json/y_string_reservedCharacterInUTF-8_U+1BFFF.json b/resources/json/y_string_reservedCharacterInUTF-8_U+1BFFF.json new file mode 100755 index 0000000..10a33a1 --- /dev/null +++ b/resources/json/y_string_reservedCharacterInUTF-8_U+1BFFF.json @@ -0,0 +1 @@ +["𛿿"] \ No newline at end of file diff --git a/resources/json/y_string_simple_ascii.json b/resources/json/y_string_simple_ascii.json new file mode 100644 index 0000000..8cadf7d --- /dev/null +++ b/resources/json/y_string_simple_ascii.json @@ -0,0 +1 @@ +["asd "] \ No newline at end of file diff --git a/resources/json/y_string_space.json b/resources/json/y_string_space.json new file mode 100644 index 0000000..efd782c --- /dev/null +++ b/resources/json/y_string_space.json @@ -0,0 +1 @@ +" " \ No newline at end of file diff --git a/resources/json/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json b/resources/json/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json new file mode 100755 index 0000000..7620b66 --- /dev/null +++ b/resources/json/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json @@ -0,0 +1 @@ +["\uD834\uDd1e"] \ No newline at end of file diff --git a/resources/json/y_string_three-byte-utf-8.json b/resources/json/y_string_three-byte-utf-8.json new file mode 100644 index 0000000..108f1d6 --- /dev/null +++ b/resources/json/y_string_three-byte-utf-8.json @@ -0,0 +1 @@ +["\u0821"] \ No newline at end of file diff --git a/resources/json/y_string_two-byte-utf-8.json b/resources/json/y_string_two-byte-utf-8.json new file mode 100644 index 0000000..461503c --- /dev/null +++ b/resources/json/y_string_two-byte-utf-8.json @@ -0,0 +1 @@ +["\u0123"] \ No newline at end of file diff --git a/resources/json/y_string_u+2028_line_sep.json b/resources/json/y_string_u+2028_line_sep.json new file mode 100755 index 0000000..897b602 --- /dev/null +++ b/resources/json/y_string_u+2028_line_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/resources/json/y_string_u+2029_par_sep.json b/resources/json/y_string_u+2029_par_sep.json new file mode 100755 index 0000000..8cd998c --- /dev/null +++ b/resources/json/y_string_u+2029_par_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/resources/json/y_string_uEscape.json b/resources/json/y_string_uEscape.json new file mode 100755 index 0000000..f7b41a0 --- /dev/null +++ b/resources/json/y_string_uEscape.json @@ -0,0 +1 @@ +["\u0061\u30af\u30EA\u30b9"] \ No newline at end of file diff --git a/resources/json/y_string_uescaped_newline.json b/resources/json/y_string_uescaped_newline.json new file mode 100644 index 0000000..3a5a220 --- /dev/null +++ b/resources/json/y_string_uescaped_newline.json @@ -0,0 +1 @@ +["new\u000Aline"] \ No newline at end of file diff --git a/resources/json/y_string_unescaped_char_delete.json b/resources/json/y_string_unescaped_char_delete.json new file mode 100755 index 0000000..7d064f4 --- /dev/null +++ b/resources/json/y_string_unescaped_char_delete.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/resources/json/y_string_unicode.json b/resources/json/y_string_unicode.json new file mode 100644 index 0000000..3598095 --- /dev/null +++ b/resources/json/y_string_unicode.json @@ -0,0 +1 @@ +["\uA66D"] \ No newline at end of file diff --git a/resources/json/y_string_unicodeEscapedBackslash.json b/resources/json/y_string_unicodeEscapedBackslash.json new file mode 100755 index 0000000..0bb3b51 --- /dev/null +++ b/resources/json/y_string_unicodeEscapedBackslash.json @@ -0,0 +1 @@ +["\u005C"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_2.json b/resources/json/y_string_unicode_2.json new file mode 100644 index 0000000..a7dcb97 --- /dev/null +++ b/resources/json/y_string_unicode_2.json @@ -0,0 +1 @@ +["â‚㈴â‚"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+10FFFE_nonchar.json b/resources/json/y_string_unicode_U+10FFFE_nonchar.json new file mode 100644 index 0000000..9a8370b --- /dev/null +++ b/resources/json/y_string_unicode_U+10FFFE_nonchar.json @@ -0,0 +1 @@ +["\uDBFF\uDFFE"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+1FFFE_nonchar.json b/resources/json/y_string_unicode_U+1FFFE_nonchar.json new file mode 100644 index 0000000..c51f8ae --- /dev/null +++ b/resources/json/y_string_unicode_U+1FFFE_nonchar.json @@ -0,0 +1 @@ +["\uD83F\uDFFE"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json b/resources/json/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json new file mode 100644 index 0000000..626d5f8 --- /dev/null +++ b/resources/json/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json @@ -0,0 +1 @@ +["\u200B"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+2064_invisible_plus.json b/resources/json/y_string_unicode_U+2064_invisible_plus.json new file mode 100644 index 0000000..1e23972 --- /dev/null +++ b/resources/json/y_string_unicode_U+2064_invisible_plus.json @@ -0,0 +1 @@ +["\u2064"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+FDD0_nonchar.json b/resources/json/y_string_unicode_U+FDD0_nonchar.json new file mode 100644 index 0000000..18ef151 --- /dev/null +++ b/resources/json/y_string_unicode_U+FDD0_nonchar.json @@ -0,0 +1 @@ +["\uFDD0"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+FFFE_nonchar.json b/resources/json/y_string_unicode_U+FFFE_nonchar.json new file mode 100644 index 0000000..13d261f --- /dev/null +++ b/resources/json/y_string_unicode_U+FFFE_nonchar.json @@ -0,0 +1 @@ +["\uFFFE"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_escaped_double_quote.json b/resources/json/y_string_unicode_escaped_double_quote.json new file mode 100755 index 0000000..4e62578 --- /dev/null +++ b/resources/json/y_string_unicode_escaped_double_quote.json @@ -0,0 +1 @@ +["\u0022"] \ No newline at end of file diff --git a/resources/json/y_string_utf8.json b/resources/json/y_string_utf8.json new file mode 100644 index 0000000..4087843 --- /dev/null +++ b/resources/json/y_string_utf8.json @@ -0,0 +1 @@ +["€ð„ž"] \ No newline at end of file diff --git a/resources/json/y_string_with_del_character.json b/resources/json/y_string_with_del_character.json new file mode 100755 index 0000000..8bd2490 --- /dev/null +++ b/resources/json/y_string_with_del_character.json @@ -0,0 +1 @@ +["aa"] \ No newline at end of file diff --git a/resources/json/y_structure_lonely_false.json b/resources/json/y_structure_lonely_false.json new file mode 100644 index 0000000..02e4a84 --- /dev/null +++ b/resources/json/y_structure_lonely_false.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/resources/json/y_structure_lonely_int.json b/resources/json/y_structure_lonely_int.json new file mode 100755 index 0000000..f70d7bb --- /dev/null +++ b/resources/json/y_structure_lonely_int.json @@ -0,0 +1 @@ +42 \ No newline at end of file diff --git a/resources/json/y_structure_lonely_negative_real.json b/resources/json/y_structure_lonely_negative_real.json new file mode 100755 index 0000000..b5135a2 --- /dev/null +++ b/resources/json/y_structure_lonely_negative_real.json @@ -0,0 +1 @@ +-0.1 \ No newline at end of file diff --git a/resources/json/y_structure_lonely_null.json b/resources/json/y_structure_lonely_null.json new file mode 100644 index 0000000..ec747fa --- /dev/null +++ b/resources/json/y_structure_lonely_null.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/resources/json/y_structure_lonely_string.json b/resources/json/y_structure_lonely_string.json new file mode 100755 index 0000000..b6e982c --- /dev/null +++ b/resources/json/y_structure_lonely_string.json @@ -0,0 +1 @@ +"asd" \ No newline at end of file diff --git a/resources/json/y_structure_lonely_true.json b/resources/json/y_structure_lonely_true.json new file mode 100755 index 0000000..f32a580 --- /dev/null +++ b/resources/json/y_structure_lonely_true.json @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/resources/json/y_structure_string_empty.json b/resources/json/y_structure_string_empty.json new file mode 100644 index 0000000..3cc762b --- /dev/null +++ b/resources/json/y_structure_string_empty.json @@ -0,0 +1 @@ +"" \ No newline at end of file diff --git a/resources/json/y_structure_trailing_newline.json b/resources/json/y_structure_trailing_newline.json new file mode 100644 index 0000000..0c3426d --- /dev/null +++ b/resources/json/y_structure_trailing_newline.json @@ -0,0 +1 @@ +["a"] diff --git a/resources/json/y_structure_true_in_array.json b/resources/json/y_structure_true_in_array.json new file mode 100644 index 0000000..de601e3 --- /dev/null +++ b/resources/json/y_structure_true_in_array.json @@ -0,0 +1 @@ +[true] \ No newline at end of file diff --git a/resources/json/y_structure_whitespace_array.json b/resources/json/y_structure_whitespace_array.json new file mode 100644 index 0000000..2bedf7f --- /dev/null +++ b/resources/json/y_structure_whitespace_array.json @@ -0,0 +1 @@ + [] \ No newline at end of file diff --git a/src/bin/fwd-browse.rs b/src/bin/fwd-browse.rs index bbebcdf..9899597 100644 --- a/src/bin/fwd-browse.rs +++ b/src/bin/fwd-browse.rs @@ -9,5 +9,10 @@ async fn main() { std::process::exit(1); } - fwd::browse_url(&args[1]).await; + let url = &args[1]; + if let Err(e) = fwd::browse_url(url).await { + eprintln!("Unable to open {url}"); + eprintln!("{}", e); + std::process::exit(1); + } } diff --git a/src/browse/mod.rs b/src/browse/mod.rs deleted file mode 100644 index cf83858..0000000 --- a/src/browse/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::message::Message; -use anyhow::Result; -use tokio::sync::mpsc; - -#[cfg(target_family = "unix")] -mod browse_unix; - -#[cfg(target_family = "unix")] -use browse_unix::{browse_url_impl, handle_browser_open_impl}; - -#[inline] -pub async fn browse_url(url: &String) { - if let Err(e) = browse_url_impl(url).await { - eprintln!("Unable to open {url}"); - eprintln!("{}", e); - std::process::exit(1); - } -} - -#[cfg(not(target_family = "unix"))] -pub async fn browse_url_impl(_url: &String) -> Result<()> { - use anyhow::anyhow; - Err(anyhow!( - "Opening a browser is not supported on this platform" - )) -} - -#[inline] -pub async fn handle_browser_open( - messages: mpsc::Sender, -) -> Result<()> { - handle_browser_open_impl(messages).await -} - -#[cfg(not(target_family = "unix"))] -async fn handle_browser_open_impl( - _messages: mpsc::Sender, -) -> Result<()> { - std::future::pending().await -} diff --git a/src/client/config.rs b/src/client/config.rs index c7f0bea..e3a3cc0 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -1,14 +1,17 @@ use anyhow::{bail, Result}; +use std::collections::hash_map; use std::collections::HashMap; -use toml::Value; +use toml::value::{Table, Value}; #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct PortConfig { pub enabled: bool, pub description: Option, } #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ServerConfig { auto: bool, ports: HashMap, @@ -20,19 +23,31 @@ impl ServerConfig { ServerConfig { auto: true, ports: HashMap::new() } } - pub fn contains_key(&self, port: u16) -> bool { - self.ports.contains_key(&port) + #[cfg(test)] + pub fn set_auto(&mut self, auto: bool) { + self.auto = auto; } - pub fn get(&self, port: u16) -> PortConfig { - match self.ports.get(&port) { - None => PortConfig { enabled: self.auto, description: None }, - Some(c) => c.clone(), - } + #[cfg(test)] + pub fn insert(&mut self, port: u16, config: PortConfig) { + self.ports.insert(port, config); + } + + pub fn auto(&self) -> bool { + self.auto + } + + pub fn iter(&self) -> hash_map::Iter { + self.ports.iter() + } + + pub fn contains_key(&self, port: u16) -> bool { + self.ports.contains_key(&port) } } #[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Config { auto: bool, servers: HashMap, @@ -50,13 +65,15 @@ impl Config { pub fn load_config() -> Result { use std::io::ErrorKind; - let mut home = match home::home_dir() { - Some(h) => h, - None => return Ok(default()), + let Some(directories) = directories_next::ProjectDirs::from("", "", "fwd") + else { + return Ok(default()); }; - home.push(".fwd"); - let contents = match std::fs::read_to_string(home) { + let mut config_path = directories.config_dir().to_path_buf(); + config_path.push("config.toml"); + + let contents = match std::fs::read_to_string(config_path) { Ok(contents) => contents, Err(e) => match e.kind() { ErrorKind::NotFound => return Ok(default()), @@ -64,95 +81,108 @@ pub fn load_config() -> Result { }, }; - Ok(parse_config(&contents.parse::()?)?) + parse_config(&contents.parse::()?) } fn default() -> Config { Config { auto: true, servers: HashMap::new() } } -fn parse_config(value: &Value) -> Result { - match value { - Value::Table(table) => Ok({ - let auto = match table.get("auto") { - None => true, - Some(Value::Boolean(v)) => *v, - Some(v) => bail!("expected a true or false, got {:?}", v), - }; - Config { - auto, - servers: get_servers(&table, auto)?, - } - }), - _ => bail!("top level must be a table"), +fn get_bool(table: &Table, key: &str, default: bool) -> Result { + match table.get(key) { + None => Ok(default), + Some(Value::Boolean(v)) => Ok(*v), + Some(v) => bail!("expected a true or false, got {v:?}"), } } -fn get_servers( - table: &toml::value::Table, +fn parse_config(value: &Value) -> Result { + let Value::Table(table) = value else { + bail!("top level must be a table") + }; + + let auto = get_bool(table, "auto", true)?; + let servers = match table.get("servers") { + None => &Table::new(), + Some(Value::Table(t)) => t, + Some(v) => bail!("Expected a table in the servers key, got {v:?}"), + }; + + Ok(Config { + auto, + servers: parse_servers(servers, auto)?, + }) +} + +fn parse_servers( + table: &Table, auto: bool, ) -> Result> { - match table.get("servers") { - None => Ok(HashMap::new()), - Some(Value::Table(table)) => Ok({ - let mut servers = HashMap::new(); - for (k, v) in table { - servers.insert(k.clone(), get_server(v, auto)?); - } - servers - }), - v => bail!("expected a table in the servers key, got {:?}", v), + let mut servers = HashMap::new(); + for (k, v) in table { + let Value::Table(table) = v else { + bail!("expected a table for server {k}, got {v:?}"); + }; + + servers.insert(k.clone(), parse_server(table, auto)?); } + Ok(servers) } -fn get_server(value: &Value, auto: bool) -> Result { +fn parse_server(table: &Table, auto: bool) -> Result { + let auto = get_bool(table, "auto", auto)?; + let ports = match table.get("ports") { + None => HashMap::new(), + Some(v) => parse_ports(v)?, + }; + + Ok(ServerConfig { auto, ports }) +} + +fn parse_ports(value: &Value) -> Result> { match value { - Value::Table(table) => Ok(ServerConfig { - auto: match table.get("auto") { - None => auto, // Default to global default - Some(Value::Boolean(v)) => *v, - Some(v) => bail!("expected true or false, got {:?}", v), - }, - ports: get_ports(table)?, - }), - value => bail!("expected a table, got {:?}", value), - } -} - -fn get_ports(table: &toml::value::Table) -> Result> { - match table.get("ports") { - None => Ok(HashMap::new()), - Some(Value::Table(table)) => Ok({ - let mut ports = HashMap::new(); - for (k,v) in table { - let port:u16 = k.parse()?; - let config = match v { - Value::Boolean(enabled) => PortConfig{enabled:*enabled, description:None}, - Value::Table(table) => PortConfig{ - enabled: match table.get("enabled") { - Some(Value::Boolean(enabled)) => *enabled, - _ => bail!("not implemented"), - }, - description: match table.get("description") { - Some(Value::String(desc)) => Some(desc.clone()), - Some(v) => bail!("expect a string description, got {:?}", v), - None => None, - }, - }, - _ => bail!("expected either a boolean (enabled) or a table for a port config, got {:?}", v), - }; - ports.insert(port, config); - } - ports - }), - Some(Value::Array(array)) => Ok({ + Value::Array(array) => { let mut ports = HashMap::new(); for v in array { - ports.insert(get_port_number(v)?, PortConfig{enabled:true, description:None}); + ports.insert( + get_port_number(v)?, + PortConfig { enabled: true, description: None }, + ); } - ports + Ok(ports) + } + + Value::Table(table) => { + let mut ports = HashMap::new(); + for (k, v) in table { + let port: u16 = k.parse()?; + let config = parse_port_config(v)?; + ports.insert(port, config); + } + Ok(ports) + } + + _ => bail!("ports must be either an array or a table, got {value:?}"), + } +} + +fn parse_port_config(value: &Value) -> Result { + match value { + Value::Boolean(enabled) => Ok(PortConfig{enabled:*enabled, description:None}), + Value::String(description) => Ok(PortConfig{ + enabled: true, + description: Some(description.clone()), }), - Some(v) => bail!("ports must be either a table of ' = ...' or an array of ports, got {:?}", v), + Value::Table(table) => { + let enabled = get_bool(table, "enabled", true)?; + let description = match table.get("description") { + Some(Value::String(desc)) => Some(desc.clone()), + Some(v) => bail!("expect a string description, got {v:?}"), + None => None, + }; + Ok(PortConfig { enabled, description }) + }, + _ => bail!("expected either a boolean (enabled), a string (description), or a table for a port config, got {value:?}"), } } @@ -163,3 +193,258 @@ fn get_port_number(v: &Value) -> Result { }; Ok(port) } + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + fn config_test(config: &str, expected: Config) { + let config = config.parse::().expect("case not toml"); + let config = parse_config(&config).expect("unable to parse config"); + + assert_eq!(expected, config); + } + + fn config_error_test(config: &str) { + let config = config.parse::().expect("case not toml"); + assert!(parse_config(&config).is_err()); + } + + #[test] + fn empty() { + config_test("", Config { auto: true, servers: HashMap::new() }); + } + + #[test] + fn auto_false() { + config_test( + " +auto=false +", + Config { auto: false, servers: HashMap::new() }, + ); + } + + #[test] + fn auto_not_boolean() { + config_error_test( + " +auto='what is going on' +", + ); + } + + #[test] + fn servers_not_table() { + config_error_test("servers=1234"); + } + + #[test] + fn servers_default() { + config_test("servers.foo={}", { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: true, ports: HashMap::new() }, + ); + Config { auto: true, servers } + }) + } + + #[test] + fn servers_auto_false() { + config_test( + " +[servers.foo] +auto=false +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: false, ports: HashMap::new() }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_auto_not_bool() { + config_error_test( + " +[servers.foo] +auto=1234 +", + ) + } + + #[test] + fn servers_ports_list() { + config_test( + " +[servers.foo] +ports=[1,2,3] +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 2, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 3, + PortConfig { enabled: true, description: None }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_table_variations() { + config_test( + " +[servers.foo.ports] +1=true +2={enabled=false} +3=false +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 2, + PortConfig { + enabled: false, + description: None, + }, + ); + ports.insert( + 3, + PortConfig { + enabled: false, + description: None, + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_table_descriptions() { + config_test( + " +[servers.foo.ports] +1={enabled=false} +2={description='humble'} +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { + enabled: false, + description: None, + }, + ); + ports.insert( + 2, + PortConfig { + enabled: true, + description: Some("humble".to_string()), + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_raw_desc() { + config_test( + " +[servers.foo.ports] +1='humble' +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { + enabled: true, + description: Some("humble".to_string()), + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_inherit_auto() { + config_test( + " +auto=false +servers.foo={} +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: false, ports: HashMap::new() }, + ); + Config { auto: false, servers } + }, + ) + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 567a186..90ec7fb 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,6 +3,7 @@ use anyhow::{bail, Result}; use bytes::BytesMut; use log::LevelFilter; use log::{debug, error, info, warn}; +use std::collections::HashMap; use std::net::{Ipv4Addr, SocketAddrV4}; use tokio::io::{ AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, @@ -15,6 +16,14 @@ use tokio::sync::mpsc; mod config; mod ui; +// 256MB clipboard. Operating systems do not generally have a maximum size +// but I need to buffer it all in memory in order to use the `copypasta` +// crate and so I want to set a limit somewhere and this seems.... rational. +// You should use another transfer mechanism other than this if you need more +// than 256 MB of data. (Obviously future me will come along and shake his +// head at the limitations of my foresight, but oh well.) +const MAX_CLIPBOARD_SIZE: usize = 256 * 1024 * 1024; + /// Wait for the server to be ready; we know the server is there and /// listening when we see the special sync marker, which is 8 NUL bytes in a /// row. @@ -52,7 +61,7 @@ async fn client_sync( } => result, }; - if let Err(_) = result { + if result.is_err() { // Something went wrong, let's just make sure we flush the client's // stderr before we return. _ = stderr.write_all(&buf[..]).await; @@ -111,7 +120,7 @@ async fn client_handle_connection( 0, // ..ho.. 1, // ..st ((port & 0xFF00) >> 8).try_into().unwrap(), // port (high) - ((port & 0x00FF) >> 0).try_into().unwrap(), // port (low) + (port & 0x00FF).try_into().unwrap(), // port (low) ]; dest_socket.write_all(&packet[..]).await?; @@ -162,25 +171,22 @@ async fn client_handle_connection( /// Listen on a port that we are currently forwarding, and use the SOCKS5 /// proxy on the specified port to handle the connections. async fn client_listen(port: u16, socks_port: u16) -> Result<()> { + let listener = + TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)).await?; loop { - let listener = - TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)) - .await?; - loop { - // The second item contains the IP and port of the new - // connection, but we don't care. - let (socket, _) = listener.accept().await?; + // The second item contains the IP and port of the new + // connection, but we don't care. + let (socket, _) = listener.accept().await?; - tokio::spawn(async move { - if let Err(e) = - client_handle_connection(socks_port, port, socket).await - { - error!("Error handling connection: {:?}", e); - } else { - debug!("Done???"); - } - }); - } + tokio::spawn(async move { + if let Err(e) = + client_handle_connection(socks_port, port, socket).await + { + error!("Error handling connection: {:?}", e); + } else { + debug!("Done???"); + } + }); } } @@ -188,20 +194,56 @@ async fn client_handle_messages( mut reader: MessageReader, events: mpsc::Sender, ) -> Result<()> { + let mut clipboard_messages = HashMap::new(); loop { use Message::*; match reader.read().await? { Ping => (), + Ports(ports) => { - if let Err(_) = events.send(ui::UIEvent::Ports(ports)).await { - // TODO: Log + if let Err(e) = events.send(ui::UIEvent::Ports(ports)).await { + error!("Error sending ports request: {:?}", e); } } + Browse(url) => { - // TODO: Uh, security? info!("Browsing to {url}..."); _ = open::that(url); } + + ClipStart(id) => { + clipboard_messages.insert(id, Vec::new()); + } + + ClipData(id, mut data) => match clipboard_messages.get_mut(&id) { + Some(bytes) => { + if bytes.len() < MAX_CLIPBOARD_SIZE { + bytes.append(&mut data); + } + } + None => { + warn!("Received data for unknown clip op {id}"); + } + }, + + ClipEnd(id) => { + let Some(data) = clipboard_messages.remove(&id) else { + warn!("Received end for unknown clip op {id}"); + continue; + }; + + let Ok(data) = String::from_utf8(data) else { + warn!("Received invalid utf8 for clipboard on op {id}"); + continue; + }; + + if let Err(e) = + events.send(ui::UIEvent::SetClipboard(data)).await + { + error!("Error sending clipboard request: {:?}", e); + } + } + message => error!("Unsupported: {:?}", message), }; } @@ -259,14 +301,14 @@ async fn client_main( } } => { if let Err(e) = result { - print!("Error sending refreshes\n"); + println!("Error sending refreshes"); return Err(e.into()); } }, result = client_handle_messages(reader, events) => { if let Err(e) = result { - print!("Error handling messages\n"); - return Err(e.into()); + println!("Error handling messages"); + return Err(e); } }, } @@ -276,6 +318,7 @@ async fn client_main( async fn spawn_ssh( server: &str, sudo: bool, + log_filter: &str, ) -> Result<(tokio::process::Child, u16), std::io::Error> { let socks_port = { let listener = TcpListener::bind("127.0.0.1:0").await?; @@ -290,7 +333,10 @@ async fn spawn_ssh( if sudo { cmd.arg("sudo"); } - cmd.arg("fwd").arg("--server"); + cmd.arg(format!("FWD_LOG={log_filter}")) + .arg("FWD_SEND_ANONYMOUS=1") + .arg("fwd") + .arg("--server"); cmd.stdout(std::process::Stdio::piped()); cmd.stdin(std::process::Stdio::piped()); @@ -320,13 +366,15 @@ fn is_sigint(status: std::process::ExitStatus) -> bool { async fn client_connect_loop( remote: &str, sudo: bool, + log_filter: &str, events: mpsc::Sender, ) { loop { _ = events.send(ui::UIEvent::Disconnected).await; - let (mut child, socks_port) = - spawn_ssh(remote, sudo).await.expect("failed to spawn"); + let (mut child, socks_port) = spawn_ssh(remote, sudo, log_filter) + .await + .expect("failed to spawn"); let mut stderr = child .stderr @@ -347,18 +395,14 @@ async fn client_connect_loop( if let Err(e) = client_sync(&mut reader, &mut stderr).await { error!("Error synchronizing: {:?}", e); - match child.wait().await { - Ok(status) => { - if is_sigint(status) { - return; - } else { - match status.code() { - Some(127) => eprintln!("Cannot find `fwd` remotely, make sure it is installed"), - _ => (), - }; - } + if let Ok(status) = child.wait().await { + if is_sigint(status) { + return; + } else if let Some(127) = status.code() { + eprintln!( + "Cannot find `fwd` remotely, make sure it is installed" + ); } - Err(_) => (), }; tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; @@ -384,13 +428,18 @@ async fn client_connect_loop( } } -pub async fn run_client(remote: &str, sudo: bool) { +pub async fn run_client(remote: &str, sudo: bool, log_filter: &str) { let (event_sender, event_receiver) = mpsc::channel(1024); _ = log::set_boxed_logger(ui::Logger::new(event_sender.clone())); log::set_max_level(LevelFilter::Info); + let server = if let Some((_user, server)) = remote.split_once("@") { + server + } else { + remote + }; let config = match config::load_config() { - Ok(config) => config.get(remote), + Ok(config) => config.get(server), Err(e) => { eprintln!("Error loading configuration: {:?}", e); return; @@ -402,7 +451,7 @@ pub async fn run_client(remote: &str, sudo: bool) { // Start the reconnect loop. tokio::select! { _ = ui.run() => (), - _ = client_connect_loop(remote, sudo, event_sender) => () + _ = client_connect_loop(remote, sudo, log_filter, event_sender) => () } } diff --git a/src/client/ui.rs b/src/client/ui.rs index efb0ca3..c29f86a 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -1,6 +1,10 @@ -use super::{client_listen, config::ServerConfig}; +use super::{ + client_listen, + config::{PortConfig, ServerConfig}, +}; use crate::message::PortDesc; use anyhow::Result; +use copypasta::{ClipboardContext, ClipboardProvider}; use crossterm::{ event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers}, execute, @@ -9,23 +13,23 @@ use crossterm::{ EnterAlternateScreen, LeaveAlternateScreen, }, }; -use log::{error, info, Level, Metadata, Record}; -use open; -use std::collections::vec_deque::VecDeque; -use std::collections::{HashMap, HashSet}; -use std::io::stdout; -use tokio::sync::mpsc; -use tokio::sync::oneshot; -use tokio_stream::StreamExt; -use tui::{ - backend::{Backend, CrosstermBackend}, +use log::{error, info, warn, Level, Metadata, Record}; +use ratatui::{ + backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Margin, Rect}, - style::{Color, Style}, + style::{Color, Modifier, Style}, widgets::{ Block, Borders, List, ListItem, ListState, Row, Table, TableState, }, Frame, Terminal, }; +use std::collections::vec_deque::VecDeque; +use std::collections::{HashMap, HashSet}; +use std::io::stdout; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio_stream::StreamExt; pub enum UIEvent { Connected(u16), @@ -33,6 +37,7 @@ pub enum UIEvent { ServerLine(String), LogLine(log::Level, String), Ports(Vec), + SetClipboard(String), } pub enum UIReturn { @@ -68,9 +73,23 @@ impl log::Log for Logger { fn flush(&self) {} } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum State { + Enabled, + Broken, + Disabled, +} + +impl State { + fn boxed(self) -> Arc> { + Arc::new(Mutex::new(self)) + } +} + #[derive(Debug)] struct Listener { - enabled: bool, + state: std::sync::Arc>, + config: Option, stop: Option>, desc: Option, } @@ -81,28 +100,89 @@ impl Listener { desc: PortDesc, enabled: bool, ) -> Listener { - let mut listener = Listener { enabled, stop: None, desc: Some(desc) }; - if enabled { - listener.start(socks_port); - } + let mut listener = Listener { + state: if enabled { + State::Enabled.boxed() + } else { + State::Disabled.boxed() + }, + config: None, + stop: None, + desc: Some(desc), + }; + listener.start(socks_port); listener } - pub fn enabled(&self) -> bool { - self.enabled + pub fn from_config(config: PortConfig) -> Self { + Listener { + state: if config.enabled { + State::Enabled.boxed() + } else { + State::Disabled.boxed() + }, + config: Some(config), + stop: None, + desc: None, + } } - pub fn set_enabled(&mut self, socks_port: Option, enabled: bool) { - if enabled { - self.enabled = true; + #[cfg(test)] + pub fn state_ref(&self) -> std::sync::Arc> { + self.state.clone() + } + + pub fn enabled(&self) -> bool { + self.state() == State::Enabled + } + + pub fn description(&self) -> &str { + if let Some(config) = self.config.as_ref() { + if let Some(description) = config.description.as_deref() { + return description; + } + } + + if let Some(port) = self.desc.as_ref() { + let desc = port.desc.as_str(); + return if desc.is_empty() { + "" + } else { + desc + }; + } + + "" + } + + pub fn is_anonymous(&self) -> bool { + // Anonynous ports are not configured and came from the server but + // had no description there. + self.config.is_none() + && match self.desc.as_ref() { + Some(desc) => desc.desc.is_empty(), + None => false, + } + } + + fn state(&self) -> State { + *self.state.lock().unwrap() + } + + pub fn toggle_enabled(&mut self, socks_port: Option) { + if self.state() == State::Disabled { + self.state = State::Enabled.boxed(); self.start(socks_port); } else { - self.enabled = false; + self.state = State::Disabled.boxed(); self.stop = None; } } pub fn connect(&mut self, socks_port: Option, desc: PortDesc) { + // If we're just sitting idle and the port comes in from the remote + // server then we should become enabled. Otherwise we should become + // real, but disabled. self.desc = Some(desc); self.start(socks_port); } @@ -113,19 +193,22 @@ impl Listener { } pub fn start(&mut self, socks_port: Option) { - if self.enabled { + if self.enabled() { if let (Some(desc), Some(socks_port), None) = (&self.desc, socks_port, &self.stop) { info!("Starting port {port} to {socks_port}", port = desc.port); let (l, stop) = oneshot::channel(); let port = desc.port; + let state = self.state.clone(); tokio::spawn(async move { let result = tokio::select! { r = client_listen(port, socks_port) => r, _ = stop => Ok(()), }; if let Err(e) = result { + let mut sg = state.lock().unwrap(); + *sg = State::Broken; error!("Error listening on port {port}: {e:?}"); } else { info!("Stopped listening on port {port}"); @@ -138,7 +221,6 @@ impl Listener { } } -#[derive(Debug)] pub struct UI { events: mpsc::Receiver, ports: HashMap, @@ -151,13 +233,22 @@ pub struct UI { show_help: bool, alternate_screen: bool, raw_mode: bool, + show_anonymous: bool, + clipboard: Option, } impl UI { pub fn new(events: mpsc::Receiver, config: ServerConfig) -> UI { + let mut ports = HashMap::new(); + for (port, config) in config.iter() { + ports.insert(*port, Listener::from_config(config.clone())); + } + + let clipboard = ClipboardContext::new().ok(); + UI { events, - ports: HashMap::new(), + ports, socks_port: None, running: true, show_logs: false, @@ -167,6 +258,8 @@ impl UI { config, alternate_screen: false, raw_mode: false, + show_anonymous: true, + clipboard, } } @@ -220,7 +313,7 @@ impl UI { Ok(code) } - fn render_connected(&mut self, frame: &mut Frame) { + fn render_connected(&mut self, frame: &mut Frame) { let constraints = if self.show_logs { vec![Constraint::Percentage(50), Constraint::Percentage(50)] } else { @@ -230,7 +323,7 @@ impl UI { let chunks = Layout::default() .direction(Direction::Vertical) .constraints(constraints) - .split(frame.size()); + .split(frame.area()); self.render_ports(frame, chunks[0]); if self.show_logs { @@ -241,9 +334,11 @@ impl UI { } } - fn render_ports(&mut self, frame: &mut Frame, size: Rect) { - let enabled_port_style = Style::default(); - let disabled_port_style = Style::default().fg(Color::DarkGray); + fn render_ports(&mut self, frame: &mut Frame, size: Rect) { + let enabled_port_style = Style::reset(); + let disabled_port_style = Style::reset().fg(Color::DarkGray); + let broken_port_style = + Style::reset().fg(Color::Red).add_modifier(Modifier::DIM); let mut rows = Vec::new(); let ports = self.get_ui_ports(); @@ -251,20 +346,22 @@ impl UI { ports.iter().map(|p| format!("{p}")).collect(); for (index, port) in ports.into_iter().enumerate() { let listener = self.ports.get(&port).unwrap(); + if !self.should_render_listener(listener) { + continue; + } + + let (symbol, style) = match listener.state() { + State::Enabled => (" ✓ ", enabled_port_style), + State::Broken => (" ✗ ", broken_port_style), + State::Disabled => ("", disabled_port_style), + }; rows.push( Row::new(vec![ - if listener.enabled { " ✓ " } else { "" }, - &port_strings[index][..], - match &listener.desc { - Some(port_desc) => &port_desc.desc, - None => "", - }, + symbol, + &*port_strings[index], + listener.description(), ]) - .style(if listener.enabled { - enabled_port_style - } else { - disabled_port_style - }), + .style(style), ); } @@ -277,17 +374,16 @@ impl UI { Constraint::Length(size.width), ]; - let port_list = Table::new(rows) + let port_list = Table::new(rows, &widths) .header(Row::new(vec!["fwd", "Port", "Description"])) .block(Block::default().title("Ports").borders(Borders::ALL)) .column_spacing(1) - .widths(&widths) .highlight_symbol(">> "); frame.render_stateful_widget(port_list, size, &mut self.selection); } - fn render_help(&mut self, frame: &mut Frame) { + fn render_help(&mut self, frame: &mut Frame) { let keybindings = vec![ Row::new(vec!["↑ / k", "Move cursor up"]), Row::new(vec!["↓ / j", "Move cursor down"]), @@ -299,6 +395,7 @@ impl UI { Row::new(vec!["ESC / q", "Quit"]), Row::new(vec!["? / h", "Show this help text"]), Row::new(vec!["l", "Show fwd's logs"]), + Row::new(vec!["a", "Hide/show anonymous ports"]), ]; let border_lines = 3; @@ -306,10 +403,10 @@ impl UI { let help_popup_area = centered_rect( 65, keybindings.len() as u16 + border_lines, - frame.size(), + frame.area(), ); let inner_area = - help_popup_area.inner(&Margin { vertical: 1, horizontal: 1 }); + help_popup_area.inner(Margin { vertical: 1, horizontal: 1 }); let key_width = 7; let binding_width = inner_area.width.saturating_sub(key_width); @@ -317,16 +414,16 @@ impl UI { Constraint::Length(key_width), Constraint::Length(binding_width), ]; - let keybindings = Table::new(keybindings) - .widths(keybindings_widths) + let keybindings = Table::new(keybindings, keybindings_widths) .column_spacing(1) - .block(Block::default().title("Keys").borders(Borders::ALL)); + .block(Block::default().title("Keys").borders(Borders::ALL)) + .style(Style::reset()); // keybindings frame.render_widget(keybindings, inner_area); } - fn render_logs(&mut self, frame: &mut Frame, size: Rect) { + fn render_logs(&mut self, frame: &mut Frame, size: Rect) { let items: Vec<_> = self.lines.iter().map(|l| ListItem::new(&l[..])).collect(); @@ -334,7 +431,7 @@ impl UI { .block(Block::default().title("Log").borders(Borders::ALL)); let mut list_state = ListState::default(); - list_state.select(if self.lines.len() > 0 { + list_state.select(if !self.lines.is_empty() { Some(self.lines.len() - 1) } else { None @@ -344,10 +441,7 @@ impl UI { } fn connected(&self) -> bool { - match self.socks_port { - Some(_) => true, - None => false, - } + self.socks_port.is_some() } fn get_ui_ports(&self) -> Vec { @@ -362,7 +456,7 @@ impl UI { fn enable_disable_port(&mut self, port: u16) { if let Some(listener) = self.ports.get_mut(&port) { - listener.set_enabled(self.socks_port, !listener.enabled()); + listener.toggle_enabled(self.socks_port); } } @@ -385,7 +479,7 @@ impl UI { fn enter_alternate_screen(&mut self) -> Result<()> { if !self.alternate_screen { enable_raw_mode()?; - execute!(stdout(), EnterAlternateScreen, DisableLineWrap)?; + execute!(stdout(), EnterAlternateScreen, DisableLineWrap,)?; self.alternate_screen = true; } Ok(()) @@ -400,6 +494,19 @@ impl UI { Ok(()) } + fn toggle_show_anonymous(&mut self) { + self.show_anonymous = !self.show_anonymous; + } + + fn should_render_listener(&self, listener: &Listener) -> bool { + // Named/Configured ports are always rendered + !listener.is_anonymous() + // ...or we might be explicitly asked to render everything + || self.show_anonymous + // ...or the port might be enabled or errored + || listener.state() != State::Disabled + } + async fn handle_events(&mut self, console_events: &mut EventStream) { tokio::select! { ev = console_events.next() => self.handle_console_event(ev), @@ -423,15 +530,14 @@ impl UI { ev: Option>, ) { match ev { - Some(Ok(Event::Key(ev))) => match ev { + Some(Ok(Event::Key( KeyEvent { code: KeyCode::Esc, .. } | KeyEvent { code: KeyCode::Char('q'), .. } | KeyEvent { code: KeyCode::Char('?'), .. } - | KeyEvent { code: KeyCode::Char('h'), .. } => { - self.show_help = false; - } - _ => (), - }, + | KeyEvent { code: KeyCode::Char('h'), .. }, + ))) => { + self.show_help = false; + } Some(Ok(_)) => (), // Don't care about this event... Some(Err(_)) => (), // Hmmmmmm.....? None => (), // ....no events? what? @@ -469,7 +575,7 @@ impl UI { | KeyEvent { code: KeyCode::Char('k'), .. } => { let index = match self.selection.selected() { Some(i) => { - assert!(self.ports.len() > 0, "We must have ports because we have a selection."); + assert!(!self.ports.is_empty(), "We must have ports because we have a selection."); if i == 0 { Some(self.ports.len() - 1) } else { @@ -477,7 +583,7 @@ impl UI { } } None => { - if self.ports.len() > 0 { + if !self.ports.is_empty() { Some(0) } else { None @@ -490,11 +596,11 @@ impl UI { | KeyEvent { code: KeyCode::Char('j'), .. } => { let index = match self.selection.selected() { Some(i) => { - assert!(self.ports.len() > 0, "We must have ports because we have a selection."); + assert!(!self.ports.is_empty(), "We must have ports because we have a selection."); Some((i + 1) % self.ports.len()) } None => { - if self.ports.len() > 0 { + if !self.ports.is_empty() { Some(0) } else { None @@ -508,6 +614,10 @@ impl UI { _ = open::that(format!("http://127.0.0.1:{}/", p)); } } + KeyEvent { code: KeyCode::Char('a'), .. } => { + self.toggle_show_anonymous() + } + _ => (), }, Some(Ok(_)) => (), // Don't care about this event... @@ -544,15 +654,22 @@ impl UI { { listener.connect(self.socks_port, port_desc); } else { - let config = self.config.get(port_desc.port); - info!("Port config {port_desc:?} -> {config:?}"); + assert!(!self.config.contains_key(port_desc.port)); + + // The server can send us these ports it knows nothing about. + // These might be dangerous to enable by default, so don't. + let enabled = if port_desc.desc.is_empty() { + false + } else { + self.config.auto() + }; self.ports.insert( port_desc.port, Listener::from_desc( self.socks_port, port_desc, - config.enabled, + enabled, ), ); } @@ -592,6 +709,18 @@ impl UI { } self.lines.push_back(format!("[CLIENT] {line}")); } + Some(UIEvent::SetClipboard(contents)) => { + let length = contents.len(); + if let Some(clipboard) = self.clipboard.as_mut() { + if let Err(e) = clipboard.set_contents(contents) { + error!("Error setting clipboard contents: {e:#}"); + } else { + info!("Received clipboard contents ({length} bytes)"); + } + } else { + warn!("No clipboard available, contents discarded"); + } + } None => { self.running = false; } @@ -606,7 +735,8 @@ impl Drop for UI { } } -/// helper function to create a centered rect using up certain percentage of the available rect `r` +/// helper function to create a centered rect using up certain percentage of +/// the available rect `r` fn centered_rect(width_chars: u16, height_chars: u16, r: Rect) -> Rect { let left = r.left() + if width_chars > r.width { @@ -630,6 +760,8 @@ fn centered_rect(width_chars: u16, height_chars: u16, r: Rect) -> Rect { #[cfg(test)] mod tests { + use crate::client::config::PortConfig; + use super::*; use assert_matches::assert_matches; @@ -894,4 +1026,376 @@ mod tests { assert_eq!(centered.width, 10); assert_eq!(centered.height, 10); } + + #[test] + fn port_config_description_respected() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8080, + PortConfig { + enabled: true, + description: Some("override".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + // There are ports... + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "my-service".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.description(), "override"); + + drop(sender); + } + + #[test] + fn port_config_enabled_respected() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8080, + PortConfig { + enabled: false, + description: Some("override".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + // There are ports... + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "my-service".to_string(), + }]))); + + let state = ui.ports.get(&8080).unwrap().state(); + assert_eq!(state, State::Disabled); + + drop(sender); + } + + #[test] + fn port_config_missing_but_still_there() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8080, + PortConfig { + enabled: false, + description: Some("override".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + // There are no ports... + ui.handle_internal_event(Some(UIEvent::Ports(vec![]))); + + // But there should still be ports, man. + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + assert_eq!(listener.description(), "override"); + + drop(sender); + } + + #[test] + fn port_config_state_interactions() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert(8080, PortConfig { enabled: false, description: None }); + config.insert(8081, PortConfig { enabled: true, description: None }); + + let mut ui = UI::new(receiver, config); + + // No ports have been received, make sure everything's in its default state. + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + + // 8080 shows up.... doesn't affect anything. + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "python3".to_string(), + }]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + + // 8081 shows up.... configured as enabled so it becomes "enabled" + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { port: 8080, desc: "python3".to_string() }, + PortDesc { port: 8081, desc: "python3".to_string() }, + ]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + + // 8082 shows up.... it should be enabled by default! + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { port: 8080, desc: "python3".to_string() }, + PortDesc { port: 8081, desc: "python3".to_string() }, + PortDesc { port: 8082, desc: "python3".to_string() }, + ]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + assert_eq!(ui.ports.get(&8082).unwrap().state(), State::Enabled); + + // 8081 goes away.... + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { port: 8080, desc: "python3".to_string() }, + PortDesc { port: 8082, desc: "python3".to_string() }, + ]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + assert_eq!(ui.ports.get(&8082).unwrap().state(), State::Enabled); + + // All gone, state resets itself. + ui.handle_internal_event(Some(UIEvent::Ports(vec![]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + assert!(!ui.ports.contains_key(&8082)); + + drop(sender); + } + + #[test] + fn port_defaults_respected() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "python3".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Enabled); + assert_eq!(listener.description(), "python3"); + + drop(sender); + } + + #[test] + fn port_default_disabled_respected() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.set_auto(false); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "python3".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + assert_eq!(listener.description(), "python3"); + + drop(sender); + } + + #[test] + fn empty_port_desc_disabled_by_default() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + assert_eq!(listener.description(), ""); + + drop(sender); + } + + #[test] + fn empty_port_desc_disabled_on_refresh() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + // Just do it again, make sure we haven't broken the refresh path. + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + drop(sender); + } + + #[test] + fn state_toggle_enable_disable() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "rando".to_string(), + }]))); + + let listener = ui.ports.get_mut(&8080).unwrap(); + assert_eq!(listener.state(), State::Enabled); + + // Enabled -> Disabled + ui.enable_disable_port(8080); // FLIP! + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + // Disabled -> Enabled + ui.enable_disable_port(8080); // FLIP! + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Enabled); + + { + // Oh no it broke! + let state = listener.state_ref(); + let mut sg = state.lock().unwrap(); + *sg = State::Broken; + } + + let listener = ui.ports.get_mut(&8080).unwrap(); + assert_eq!(listener.state(), State::Broken); + + // Broken -> Disabled + ui.enable_disable_port(8080); + let listener = ui.ports.get_mut(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + drop(sender); + } + + #[test] + fn listener_anonymous() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8079, + PortConfig { + enabled: false, + description: Some("body once told me".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { + port: 8080, + desc: "python3 blaster.py".to_string(), + }, + PortDesc { port: 8081, desc: "".to_string() }, + PortDesc { port: 8082, desc: "".to_string() }, + ]))); + + // (Pretend that 8082 broke.) + ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed(); + + let listener = ui.ports.get(&8079).unwrap(); + assert!( + !listener.is_anonymous(), + "Configured ports should not be anonymous" + ); + + let listener = ui.ports.get(&8080).unwrap(); + assert!( + !listener.is_anonymous(), + "Ports with descriptions should not be anonymous" + ); + + let listener = ui.ports.get(&8081).unwrap(); + assert!( + listener.is_anonymous(), + "Not configured, disabled, no description should be anonymous" + ); + + drop(sender); + } + + #[test] + fn render_anonymous() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8079, + PortConfig { + enabled: false, + description: Some("body once told me".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { + port: 8080, + desc: "python3 blaster.py".to_string(), + }, + PortDesc { port: 8081, desc: "".to_string() }, + PortDesc { port: 8082, desc: "".to_string() }, + PortDesc { port: 8083, desc: "".to_string() }, + ]))); + + // (Pretend that 8082 broke.) + ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed(); + + // No showing anonymous ports! + ui.show_anonymous = false; + + let listener = ui.ports.get(&8079).unwrap(); + assert!( + ui.should_render_listener(listener), + "Configured ports should always be rendered" + ); + + let listener = ui.ports.get(&8080).unwrap(); + assert!( + ui.should_render_listener(listener), + "Ports with descriptions should be rendered" + ); + + let listener = ui.ports.get(&8081).unwrap(); + assert!( + !ui.should_render_listener(listener), + "Not configured, disabled, no description should be hidden" + ); + + ui.enable_disable_port(8081); + + let listener = ui.ports.get(&8081).unwrap(); + assert_eq!(listener.state(), State::Enabled); + assert!( + ui.should_render_listener(listener), + "Enabled ports should be rendered" + ); + + let listener = ui.ports.get(&8082).unwrap(); + assert_eq!(listener.state(), State::Broken); + assert!( + ui.should_render_listener(listener), + "Broken ports should be rendered" + ); + + drop(sender); + } } diff --git a/src/lib.rs b/src/lib.rs index 5eaba0d..d90a75e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,13 @@ -mod browse; mod client; mod message; -mod server; +mod reverse; +pub mod server; + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const REV: &str = env!("REPO_REV"); +pub const DIRTY: &str = env!("REPO_DIRTY"); -pub use browse::browse_url; pub use client::run_client; +pub use reverse::browse_url; +pub use reverse::clip_file; pub use server::run_server; diff --git a/src/main.rs b/src/main.rs index 667ff1c..a6201e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,9 @@ // TODO: An actual proper command line parsing use indoc::indoc; -const VERSION: &str = env!("CARGO_PKG_VERSION"); - fn usage() { println!(indoc! {" -usage: fwd [options] ( | browse ) +usage: fwd [options] ( | browse | clip []) To connect a client to a server that has an `fwd` installed in its path, run `fwd ` on the client, where is the name of the server to @@ -14,14 +12,22 @@ connect to. On a server that already has a client connected to it you can use `fwd browse ` to open `` in the default browser of the client. +On a server that already has a client connected to it you can use `fwd clip -` +to read stdin and send it to the clipboard of the client, or `fwd clip ` +to send the the contents of `file`. + Options: --version Print the version of fwd and exit --sudo, -s Run the server side of fwd with `sudo`. This allows the - client to forward ports that are open by processes being - run under other accounts (e.g., docker containers being - run as root), but requires sudo access on the server and - *might* end up forwarding ports that you do not want + client to identify the ports that are open by processes + being run under other accounts (e.g., docker containers + being run as root), but requires sudo access on the server + and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.) + --log-filter FILTER + Set remote server's log level. Default is `warn`. Supports + all of Rust's env_logger filter syntax, e.g. + `--log-filter=fwd::trace`. "}); } @@ -30,17 +36,20 @@ enum Args { Help, Version, Server, - Client(String, bool), + Client(String, bool, String), Browse(String), + Clip(String), Error, } fn parse_args(args: Vec) -> Args { let mut server = None; let mut sudo = None; + let mut log_filter = None; let mut rest = Vec::new(); - for arg in args.into_iter().skip(1) { + let mut arg_iter = args.into_iter().skip(1); + while let Some(arg) = arg_iter.next() { if arg == "--help" || arg == "-?" || arg == "-h" { return Args::Help; } else if arg == "--version" { @@ -49,30 +58,78 @@ fn parse_args(args: Vec) -> Args { server = Some(true) } else if arg == "--sudo" || arg == "-s" { sudo = Some(true) + } else if arg.starts_with("--log-filter") { + if arg.contains('=') { + log_filter = Some(arg.split('=').nth(1).unwrap().to_owned()); + } else if let Some(arg) = arg_iter.next() { + log_filter = Some(arg); + } else { + return Args::Error; + } } else { rest.push(arg) } } if server.unwrap_or(false) { - if rest.len() == 0 && sudo.is_none() { + if rest.is_empty() && sudo.is_none() { Args::Server } else { Args::Error } - } else if rest.len() > 1 && rest[0] == "browse" { + } else if rest.is_empty() { + Args::Error + } else if rest[0] == "browse" { if rest.len() == 2 { Args::Browse(rest[1].to_string()) + } else if rest.len() == 1 { + Args::Client( + rest[0].to_string(), + sudo.unwrap_or(false), + log_filter.unwrap_or("warn".to_owned()), + ) + } else { + Args::Error + } + } else if rest[0] == "clip" { + if rest.len() == 1 { + Args::Client( + rest[0].to_string(), + sudo.unwrap_or(false), + log_filter.unwrap_or("warn".to_owned()), + ) + } else if rest.len() == 2 { + Args::Clip(rest[1].to_string()) } else { Args::Error } } else if rest.len() == 1 { - Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) + Args::Client( + rest[0].to_string(), + sudo.unwrap_or(false), + log_filter.unwrap_or("warn".to_owned()), + ) } else { Args::Error } } +async fn browse_url(url: &str) { + if let Err(e) = fwd::browse_url(url).await { + eprintln!("Unable to open {url}"); + eprintln!("{:#}", e); + std::process::exit(1); + } +} + +async fn clip_file(file: String) { + if let Err(e) = fwd::clip_file(&file).await { + eprintln!("Unable to copy to the clipboard"); + eprintln!("{:#}", e); + std::process::exit(1); + } +} + #[tokio::main] async fn main() { match parse_args(std::env::args().collect()) { @@ -80,16 +137,19 @@ async fn main() { usage(); } Args::Version => { - println!("fwd {VERSION}"); + println!("fwd {} (rev {}{})", fwd::VERSION, fwd::REV, fwd::DIRTY); } Args::Server => { fwd::run_server().await; } Args::Browse(url) => { - fwd::browse_url(&url).await; + browse_url(&url).await; } - Args::Client(server, sudo) => { - fwd::run_client(&server, sudo).await; + Args::Clip(file) => { + clip_file(file).await; + } + Args::Client(server, sudo, log_filter) => { + fwd::run_client(&server, sudo, &log_filter).await; } Args::Error => { usage(); @@ -105,8 +165,7 @@ mod tests { // Goldarn it. fn args(x: &[&str]) -> Vec { - let mut vec: Vec = - x.into_iter().map(|a| a.to_string()).collect(); + let mut vec: Vec = x.iter().map(|a| a.to_string()).collect(); vec.insert(0, "fwd".to_string()); vec } @@ -131,6 +190,7 @@ mod tests { fn errors() { assert_arg_parse!(&[], Args::Error); assert_arg_parse!(&["browse", "google.com", "what"], Args::Error); + assert_arg_parse!(&["clip", "a.txt", "b.txt"], Args::Error); assert_arg_parse!(&["a", "b"], Args::Error); assert_arg_parse!(&["--server", "something"], Args::Error); assert_arg_parse!(&["--server", "--sudo"], Args::Error); @@ -139,12 +199,39 @@ mod tests { #[test] fn client() { - assert_arg_parse!(&["foo.com"], Args::Client(_, false)); - assert_arg_parse!(&["a"], Args::Client(_, false)); - assert_arg_parse!(&["browse"], Args::Client(_, false)); - assert_arg_parse!(&["foo.com", "--sudo"], Args::Client(_, true)); - assert_arg_parse!(&["a", "-s"], Args::Client(_, true)); - assert_arg_parse!(&["-s", "browse"], Args::Client(_, true)); + assert_arg_parse!(&["foo.com"], Args::Client(_, false, _)); + assert_arg_parse!(&["a"], Args::Client(_, false, _)); + assert_arg_parse!(&["browse"], Args::Client(_, false, _)); + assert_arg_parse!(&["clip"], Args::Client(_, false, _)); + assert_arg_parse!(&["foo.com", "--sudo"], Args::Client(_, true, _)); + assert_arg_parse!(&["a", "-s"], Args::Client(_, true, _)); + assert_arg_parse!(&["-s", "browse"], Args::Client(_, true, _)); + assert_arg_parse!(&["-s", "clip"], Args::Client(_, true, _)); + + assert_client_parse(&["a"], "a", false, "warn"); + assert_client_parse(&["a", "--log-filter", "info"], "a", false, "info"); + assert_client_parse(&["a", "--log-filter=info"], "a", false, "info"); + assert_client_parse( + &["a", "--sudo", "--log-filter=info"], + "a", + true, + "info", + ); + } + + fn assert_client_parse( + x: &[&str], + server: &str, + sudo: bool, + log_filter: &str, + ) { + let args = parse_args(args(x)); + assert_matches!(args, Args::Client(_, _, _)); + if let Args::Client(s, sdo, lf) = args { + assert_eq!(s, server); + assert_eq!(sdo, sudo); + assert_eq!(lf, log_filter); + } } #[test] @@ -152,6 +239,12 @@ mod tests { assert_arg_parse!(&["--server"], Args::Server); } + #[test] + fn clip() { + assert_arg_parse!(&["clip", "garbage"], Args::Clip(_)); + assert_arg_parse!(&["clip", "-"], Args::Clip(_)); + } + #[test] fn browse() { assert_arg_parse!(&["browse", "google.com"], Args::Browse(_)); diff --git a/src/message.rs b/src/message.rs index eb5efd8..f347727 100644 --- a/src/message.rs +++ b/src/message.rs @@ -57,6 +57,11 @@ pub enum Message { // Browse a thing Browse(String), + + // Send data to the remote clipboard + ClipStart(u64), + ClipData(u64, Vec), + ClipEnd(u64), } impl Message { @@ -94,8 +99,7 @@ impl Message { result.put_u16(port.port); // Port descriptions can be long, let's make sure they're not. - let sliced = - slice_up_to(&port.desc, u16::max_value().into()); + let sliced = slice_up_to(&port.desc, u16::MAX.into()); put_string(result, sliced); } } @@ -103,6 +107,19 @@ impl Message { result.put_u8(0x07); put_string(result, url); } + ClipStart(id) => { + result.put_u8(0x08); + result.put_u64(*id); + } + ClipData(id, data) => { + result.put_u8(0x09); + result.put_u64(*id); + put_data(result, data); + } + ClipEnd(id) => { + result.put_u8(0x0A); + result.put_u64(*id); + } }; } @@ -132,7 +149,20 @@ impl Message { Ok(Ports(ports)) } 0x07 => Ok(Browse(get_string(cursor)?)), - b => Err(Error::Unknown(b).into()), + 0x08 => { + let id = get_u64(cursor)?; + Ok(ClipStart(id)) + } + 0x09 => { + let id = get_u64(cursor)?; + let data = get_data(cursor)?; + Ok(Self::ClipData(id, data)) + } + 0x0A => { + let id = get_u64(cursor)?; + Ok(ClipEnd(id)) + } + b => Err(Error::Unknown(b)), } } } @@ -151,6 +181,13 @@ fn get_u16(cursor: &mut Cursor<&[u8]>) -> Result { Ok(cursor.get_u16()) } +fn get_u64(cursor: &mut Cursor<&[u8]>) -> Result { + if cursor.remaining() < 8 { + return Err(Error::Incomplete); + } + Ok(cursor.get_u64()) +} + fn get_bytes(cursor: &mut Cursor<&[u8]>, length: usize) -> Result { if cursor.remaining() < length { return Err(Error::Incomplete); @@ -182,6 +219,22 @@ fn put_string(target: &mut T, str: &str) { target.put_slice(str.as_bytes()); } +fn put_data(target: &mut T, data: &[u8]) { + target.put_u16(data.len().try_into().expect("Buffer is too long")); + target.put_slice(data); +} + +fn get_data(cursor: &mut Cursor<&[u8]>) -> Result> { + let length = get_u16(cursor)?; + if cursor.remaining() < length.into() { + return Err(Error::Incomplete); + } + + let mut data: Vec = vec![0; length.into()]; + cursor.copy_to_slice(&mut data); + Ok(data) +} + // ---------------------------------------------------------------------------- // Message IO @@ -193,14 +246,14 @@ impl MessageWriter { pub fn new(writer: T) -> MessageWriter { MessageWriter { writer } } - pub async fn write(self: &mut Self, msg: Message) -> Result<()> { + pub async fn write(&mut self, msg: Message) -> Result<()> { // TODO: Optimize buffer usage please this is bad // eprintln!("? {:?}", msg); - let mut buffer = msg.encode(); + let buffer = msg.encode(); self.writer .write_u32(buffer.len().try_into().expect("Message too large")) .await?; - self.writer.write_all(&mut buffer).await?; + self.writer.write_all(&buffer).await?; self.writer.flush().await?; Ok(()) } @@ -214,7 +267,8 @@ impl MessageReader { pub fn new(reader: T) -> MessageReader { MessageReader { reader } } - pub async fn read(self: &mut Self) -> Result { + + pub async fn read(&mut self) -> Result { let frame_length = self.reader.read_u32().await?; let mut data = vec![0; frame_length.try_into().unwrap()]; self.reader.read_exact(&mut data).await?; @@ -283,6 +337,12 @@ mod message_tests { }, ])); assert_round_trip(Browse("https://google.com/".to_string())); + assert_round_trip(ClipStart(0x1234567890ABCDEF)); + assert_round_trip(ClipData( + 0x1234567890ABCDEF, + vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + )); + assert_round_trip(ClipEnd(0x1234567890ABCDEF)); } #[test] diff --git a/src/reverse.rs b/src/reverse.rs new file mode 100644 index 0000000..971d274 --- /dev/null +++ b/src/reverse.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use rand::random; +use tokio::io::{AsyncRead, AsyncReadExt}; + +#[cfg(target_family = "unix")] +mod unix; + +#[cfg(target_family = "unix")] +pub use unix::{handle_reverse_connections, ReverseConnection}; + +use crate::message::Message; + +#[cfg(not(target_family = "unix"))] +pub struct ReverseConnection {} + +#[cfg(not(target_family = "unix"))] +impl ReverseConnection { + pub async fn new() -> Result { + use anyhow::anyhow; + Err(anyhow!( + "Server-side operations are not supported on this platform" + )) + } + + pub async fn send(&mut self, _message: Message) -> Result<()> { + use anyhow::anyhow; + Err(anyhow!( + "Server-side operations are not supported on this platform" + )) + } +} + +#[cfg(not(target_family = "unix"))] +pub async fn handle_reverse_connections( + _messages: tokio::sync::mpsc::Sender, +) -> Result<()> { + std::future::pending().await +} + +#[inline] +pub async fn browse_url(url: &str) -> Result<()> { + ReverseConnection::new() + .await? + .send(Message::Browse(url.to_string())) + .await +} + +async fn clip_reader(reader: &mut T) -> Result<()> { + let mut connection = ReverseConnection::new().await?; + let clip_id: u64 = random(); + connection.send(Message::ClipStart(clip_id)).await?; + + let mut count = 0; + let mut buf = vec![0; 1024]; + loop { + let read = reader.read(&mut buf[count..]).await?; + if read == 0 { + break; + } + count += read; + if count == buf.len() { + connection.send(Message::ClipData(clip_id, buf)).await?; + buf = vec![0; 1024]; + count = 0; + } + } + + if count > 0 { + buf.resize(count, 0); + connection.send(Message::ClipData(clip_id, buf)).await?; + } + + connection.send(Message::ClipEnd(clip_id)).await?; + Ok(()) +} + +#[inline] +pub async fn clip_file(file: &str) -> Result<()> { + if file == "-" { + let mut stdin = tokio::io::stdin(); + clip_reader(&mut stdin).await?; + } else { + let mut file = tokio::fs::File::open(file).await?; + clip_reader(&mut file).await?; + } + + Ok(()) +} diff --git a/src/browse/browse_unix.rs b/src/reverse/unix.rs similarity index 61% rename from src/browse/browse_unix.rs rename to src/reverse/unix.rs index 54cca9c..a968edd 100644 --- a/src/browse/browse_unix.rs +++ b/src/reverse/unix.rs @@ -1,37 +1,75 @@ -use crate::message::{Message, MessageReader, MessageWriter}; +// The reverse client connects to the server via a local connection to send +// commands back to the client. use anyhow::{bail, Context, Result}; use log::warn; use std::os::unix::fs::DirBuilderExt; use std::path::PathBuf; use tokio::net::{UnixListener, UnixStream}; use tokio::sync::mpsc; -use users; -use xdg; -pub async fn browse_url_impl(url: &String) -> Result<()> { - let path = socket_path().context("Error getting socket path")?; - let stream = match UnixStream::connect(&path).await { - Ok(s) => s, - Err(e) => bail!( - "Error connecting to socket: {e} (is fwd actually connected here?)" - ), - }; - let mut writer = MessageWriter::new(stream); - writer - .write(Message::Browse(url.clone())) - .await - .context("Error sending browse message")?; - Ok(()) +use crate::message::{Message, MessageReader, MessageWriter}; + +pub struct ReverseConnection { + writer: MessageWriter, } -pub async fn handle_browser_open_impl( +impl ReverseConnection { + pub async fn new() -> Result { + let path = socket_path().context("Error getting socket path")?; + let stream = match UnixStream::connect(&path).await { + Ok(s) => s, + Err(e) => bail!("Error connecting to socket: {e} (is fwd actually connected here?)"), + }; + + Ok(ReverseConnection { writer: MessageWriter::new(stream) }) + } + + pub async fn send(&mut self, message: Message) -> Result<()> { + self.writer + .write(message) + .await + .context("Error sending reverse message")?; + Ok(()) + } +} + +pub fn socket_path() -> Result { + let mut socket_path = socket_directory()?; + + std::fs::DirBuilder::new() + .recursive(true) + .mode(0o700) + .create(&socket_path) + .context("Error creating socket directory")?; + + // TODO: check mode of directory + + socket_path.push("browser"); + Ok(socket_path) +} + +fn socket_directory() -> Result { + match directories_next::ProjectDirs::from("", "", "fwd") + .and_then(|p| p.runtime_dir().map(|p| p.to_path_buf())) + { + Some(p) => Ok(p), + None => { + let mut path = std::env::temp_dir(); + let uid = unsafe { libc::getuid() }; + path.push(format!("fwd{}", uid)); + Ok(path) + } + } +} + +pub async fn handle_reverse_connections( messages: mpsc::Sender, ) -> Result<()> { let path = socket_path().context("Error getting socket path")?; - handle_browser_open_with_path(messages, path).await + handle_reverse_connections_with_path(messages, path).await } -async fn handle_browser_open_with_path( +async fn handle_reverse_connections_with_path( messages: mpsc::Sender, path: PathBuf, ) -> Result<()> { @@ -53,43 +91,23 @@ async fn handle_browser_open_with_path( } } -pub fn socket_path() -> Result { - let mut socket_path = socket_directory()?; - - std::fs::DirBuilder::new() - .recursive(true) - .mode(0o700) - .create(&socket_path) - .context("Error creating socket directory")?; - - // TODO: check mode of directory - - socket_path.push("browser"); - Ok(socket_path) -} - -fn socket_directory() -> Result { - let base_directories = xdg::BaseDirectories::new() - .context("Error creating BaseDirectories")?; - match base_directories.place_runtime_file("fwd") { - Ok(path) => Ok(path), - Err(_) => { - let mut path = std::env::temp_dir(); - path.push(format!("fwd{}", users::get_current_uid())); - Ok(path) - } - } -} - async fn handle_connection( socket: UnixStream, sender: mpsc::Sender, ) -> Result<()> { let mut reader = MessageReader::new(socket); - let message = reader.read().await.context("Error reading message")?; - match message { - Message::Browse(url) => sender.send(Message::Browse(url)).await?, - _ => bail!("Unsupported message: {:?}", message), + while let Ok(message) = reader.read().await { + match message { + Message::Browse(url) => sender.send(Message::Browse(url)).await?, + Message::ClipStart(id) => { + sender.send(Message::ClipStart(id)).await? + } + Message::ClipData(id, data) => { + sender.send(Message::ClipData(id, data)).await? + } + Message::ClipEnd(id) => sender.send(Message::ClipEnd(id)).await?, + _ => bail!("Unsupported message: {:?}", message), + } } Ok(()) @@ -99,7 +117,7 @@ async fn handle_connection( mod tests { use super::*; use crate::message::MessageWriter; - use tempdir::TempDir; + use tempfile::TempDir; #[test] fn socket_path_repeats() { @@ -113,13 +131,13 @@ mod tests { async fn url_to_message() { let (sender, mut receiver) = mpsc::channel(64); - let tmp_dir = - TempDir::new("url_to_message").expect("Error getting tmpdir"); + let tmp_dir = TempDir::with_prefix("url_to_message") + .expect("Error getting tmpdir"); let path = tmp_dir.path().join("socket"); let path_override = path.clone(); tokio::spawn(async move { - handle_browser_open_with_path(sender, path_override) + handle_reverse_connections_with_path(sender, path_override) .await .expect("Error in server!"); }); diff --git a/src/server/mod.rs b/src/server/mod.rs index 1b1f1b2..db56ce2 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,11 +1,11 @@ -use crate::browse::handle_browser_open; use crate::message::{Message, MessageReader, MessageWriter}; +use crate::reverse::handle_reverse_connections; use anyhow::Result; use log::{error, warn}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; use tokio::sync::mpsc; -mod refresh; +pub mod refresh; // We drive writes through an mpsc queue, because we not only handle requests // and responses from the client (refresh ports and the like) but also need @@ -13,7 +13,7 @@ mod refresh; async fn write_driver( messages: &mut mpsc::Receiver, writer: &mut MessageWriter, -) -> () { +) { while let Some(m) = messages.recv().await { writer.write(m).await.expect("Failed to write the message") } @@ -24,15 +24,38 @@ async fn server_loop( reader: &mut MessageReader, writer: &mut mpsc::Sender, ) -> Result<()> { + // NOTE: The client needs to opt in to getting anonymous ports because it + // does not feel safe to automatically enable port forwarding by default + // for random system ports. The way we keep it from being unsafe is that + // the client leaves anonymous ports disabled by default. Older clients + // did not do this, and so we cannot send older clients anonymous ports. + let send_anonymous = std::env::var("FWD_SEND_ANONYMOUS") + .map(|v| v == "1") + .unwrap_or(false); + // The first message we send must be an announcement. writer.send(Message::Hello(0, 2, vec![])).await?; - + let mut version_reported = false; loop { use Message::*; match reader.read().await? { Ping => (), Refresh => { - let ports = match refresh::get_entries() { + // Just log the version, if we haven't yet. We do this extra + // work to avoid spamming the log, but we wait until we + // receive the first message to be sure that the client is in + // a place to display our logging properly. + if !version_reported { + eprintln!( + "fwd server {} (rev {}{})", + crate::VERSION, + crate::REV, + crate::DIRTY + ); + version_reported = true; + } + + let ports = match refresh::get_entries(send_anonymous).await { Ok(ports) => ports, Err(e) => { error!("Error scanning: {:?}", e); @@ -77,11 +100,15 @@ async fn server_main< tokio::select! { _ = write_driver(&mut receiver, &mut writer) => Ok(()), r = server_loop(&mut reader, &mut sender) => r, - r = handle_browser_open(browse_sender) => r, + r = handle_reverse_connections(browse_sender) => r, } } pub async fn run_server() { + env_logger::Builder::from_env( + env_logger::Env::new().filter_or("FWD_LOG", "warn"), + ) + .init(); let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); if let Err(e) = server_main(stdin, stdout).await { @@ -96,7 +123,7 @@ mod tests { use tokio::io::{AsyncReadExt, DuplexStream}; async fn sync(client_read: &mut DuplexStream) { - print!("[client] Waiting for server sync...\n"); + println!("[client] Waiting for server sync..."); for _ in 0..8 { let b = client_read .read_u8() @@ -106,7 +133,7 @@ mod tests { } let mut reader = MessageReader::new(client_read); - print!("[client] Reading first message...\n"); + println!("[client] Reading first message..."); let msg = reader.read().await.expect("Error reading first message"); assert_matches!(msg, Message::Hello(0, 2, _)); } diff --git a/src/server/refresh.rs b/src/server/refresh.rs index e21cf6f..a1a3d05 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -1,65 +1,53 @@ -use crate::message::PortDesc; use anyhow::Result; +#[cfg_attr(not(target_os = "linux"), allow(unused))] +use log::error; +use log::warn; +use std::collections::HashMap; -#[cfg(not(target_os = "linux"))] -pub fn get_entries() -> Result> { - use anyhow::bail; - bail!("Not supported on this operating system"); -} +use crate::message::PortDesc; #[cfg(target_os = "linux")] -pub fn get_entries() -> Result> { - use procfs::process::FDTarget; - use std::collections::HashMap; +mod procfs; - let all_procs = procfs::process::all_processes()?; +#[cfg(unix)] +pub mod docker; - // build up a map between socket inodes and process stat info. Ignore any - // error we encounter as it probably means we have no access to that - // process or something. - let mut map: HashMap = HashMap::new(); - for p in all_procs { - if let Ok(process) = p { - if !process.is_alive() { - continue; // Ignore zombies. - } +pub async fn get_entries(_send_anonymous: bool) -> Result> { + #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] + let mut attempts = 0; - if let (Ok(fds), Ok(cmd)) = (process.fd(), process.cmdline()) { - for fd in fds { - if let Ok(fd) = fd { - if let FDTarget::Socket(inode) = fd.target { - map.insert(inode, cmd.join(" ")); - } - } + #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] + let mut result: HashMap = HashMap::new(); + + #[cfg(unix)] + { + attempts += 1; + match docker::get_entries().await { + Ok(m) => { + for (p, d) in m { + result.entry(p).or_insert(d); } } + Err(e) => error!("Error reading from docker: {e:?}"), } } - let mut h: HashMap = HashMap::new(); - - // Go through all the listening IPv4 and IPv6 sockets and take the first - // instance of listening on each port *if* the address is loopback or - // unspecified. (TODO: Do we want this restriction really?) - let tcp = procfs::net::tcp()?; - let tcp6 = procfs::net::tcp6()?; - for tcp_entry in tcp.into_iter().chain(tcp6) { - if tcp_entry.state == procfs::net::TcpState::Listen - && (tcp_entry.local_address.ip().is_loopback() - || tcp_entry.local_address.ip().is_unspecified()) - && !h.contains_key(&tcp_entry.local_address.port()) - { - if let Some(cmd) = map.get(&tcp_entry.inode) { - h.insert( - tcp_entry.local_address.port(), - PortDesc { - port: tcp_entry.local_address.port(), - desc: cmd.clone(), - }, - ); + #[cfg(target_os = "linux")] + { + attempts += 1; + match procfs::get_entries(_send_anonymous) { + Ok(m) => { + for (p, d) in m { + result.entry(p).or_insert(d); + } } + Err(e) => error!("Error reading procfs: {e:?}"), } } - Ok(h.into_values().collect()) + if attempts == 0 { + warn!("Port scanning is not supported for this server"); + } + + Ok(result.into_values().collect()) } diff --git a/src/server/refresh/docker.rs b/src/server/refresh/docker.rs new file mode 100644 index 0000000..06d4fff --- /dev/null +++ b/src/server/refresh/docker.rs @@ -0,0 +1,1029 @@ +use anyhow::{bail, Context, Result}; +use log::trace; +use std::collections::HashMap; +use tokio::io::{ + AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, +}; + +use crate::message::PortDesc; + +pub const DEFAULT_DOCKER_HOST: &str = "unix:///var/run/docker.sock"; + +async fn list_containers_with_connection(stream: T) -> Result> +where + T: AsyncRead + AsyncWrite + Unpin, +{ + // Send this one exact request. (Who needs an HTTP library?) + const DOCKER_LIST_CONTAINERS: &[u8] = b"\ +GET /containers/json HTTP/1.1\r\n\ +Host: localhost\r\n\ +User-Agent: fwd/1.0\r\n\ +Accept: */*\r\n\ +\r\n"; + + let mut stream = tokio::io::BufStream::new(stream); + stream.write_all(DOCKER_LIST_CONTAINERS).await?; + stream.flush().await?; + + // Check the HTTP response. + let mut line = String::new(); + stream.read_line(&mut line).await?; + trace!("[docker] {}", &line.trim_end()); + let parts: Vec<&str> = line.split(" ").collect(); + if parts.len() < 2 || parts[1] != "200" { + bail!("Error response from docker: {line:?}"); + } + + // Process the headers; all we really care about is content-length or content-encoding. + let mut content_length: Option = None; + let mut chunked = false; + loop { + line.clear(); + stream.read_line(&mut line).await?; + trace!("[docker] {}", line.trim_end()); + if line.trim().is_empty() { + break; + } + line.make_ascii_lowercase(); + if let Some(rest) = line.strip_prefix("content-length: ") { + content_length = Some(rest.trim().parse()?); + } + if let Some(rest) = line.strip_prefix("transfer-encoding: ") { + chunked = rest.trim() == "chunked"; + } + } + + // Read the JSON response. + let mut response_buffer = vec![0; content_length.unwrap_or(0)]; + if content_length.is_some() { + stream.read_exact(&mut response_buffer).await?; + } else if chunked { + // Docker will send a chunked encoding if the response seems too big to do + // all at once. I don't know the heuristic it uses but we need to deal with + // it. Fortunately chunked encoding is not too bad? + loop { + line.clear(); + stream.read_line(&mut line).await?; + // This is the hex length of the thing. + let Some(chunk_length) = line.split(";").next() else { + bail!("Can't make sense of chunk length line: {line:?}"); + }; + let Ok(chunk_length) = + usize::from_str_radix(chunk_length.trim(), 16) + else { + bail!("Cannot interpret chunk length '{chunk_length}' as hex (Full line: {line:?})"); + }; + if chunk_length > 0 { + let old_length = response_buffer.len(); + let new_length = old_length + chunk_length; + response_buffer.resize(new_length, 0); + stream + .read_exact(&mut response_buffer[old_length..new_length]) + .await?; + } + + let mut eol: [u8; 2] = [0, 0]; + stream.read_exact(&mut eol).await?; + if eol[0] != b'\r' || eol[1] != b'\n' { + bail!("Mal-formed end-of-chunk marker from server"); + } + if chunk_length == 0 { + break; // All done. + } + } + } else { + trace!("Docker did not send a content_length, just reading to the end"); + stream.read_to_end(&mut response_buffer).await?; + } + + if log::log_enabled!(log::Level::Trace) { + match std::str::from_utf8(&response_buffer) { + Ok(s) => trace!("[docker][{}b] {}", s.len(), s), + Err(_) => trace!( + "[docker][{}b, raw] {:?}", + response_buffer.len(), + &response_buffer + ), + } + } + + // Done with the stream. + Ok(response_buffer) +} + +async fn list_containers() -> Result> { + let host = std::env::var("DOCKER_HOST") + .unwrap_or_else(|_| DEFAULT_DOCKER_HOST.to_string()); + trace!("[docker] Connecting to {host}"); + match host { + h if h.starts_with("unix://") => { + let socket_path = &h[7..]; + let socket = tokio::net::UnixStream::connect(socket_path).await?; + list_containers_with_connection(socket).await + } + h if h.starts_with("tcp://") => { + let host_port = &h[6..]; // TODO: Routing to sub-paths? + let socket = tokio::net::TcpStream::connect(host_port).await?; + list_containers_with_connection(socket).await + } + h if h.starts_with("http://") => { + let host_port = &h[7..]; // TODO: Routing to sub-paths? + let socket = tokio::net::TcpStream::connect(host_port).await?; + list_containers_with_connection(socket).await + } + _ => bail!("Unsupported docker host: {host}"), + } +} + +#[derive(Debug, PartialEq)] +pub enum JsonValue { + Null, + True, + False, + Number(f64), + String(String), + Object(HashMap), + Array(Vec), +} + +/// If the characters at `chars` match the characters in `prefix`, consume +/// those and return true. Otherwise, return false and do not advance the +/// iterator. +fn matches(chars: &mut std::str::Chars, prefix: &str) -> bool { + let backup = chars.clone(); + for c in prefix.chars() { + if chars.next() != Some(c) { + *chars = backup; + return false; + } + } + true +} + +impl JsonValue { + pub fn parse(blob: &[u8]) -> Result { + Self::parse_impl(blob).with_context(|| { + match std::str::from_utf8(blob) { + Ok(s) => format!("Failed to parse {} bytes: '{}'", s.len(), s), + Err(_) => { + format!( + "Failed to parse {} bytes (not utf-8): {:?}", + blob.len(), + blob + ) + } + } + }) + } + + fn parse_impl(blob: &[u8]) -> Result { + enum Tok { + Val(JsonValue), + StartObject, + StartArray, + } + + let mut stack = Vec::new(); + let mut i = 0; + while i < blob.len() { + match blob[i] { + b'n' => { + i += 4; + stack.push(Tok::Val(JsonValue::Null)); + } + b't' => { + i += 4; + stack.push(Tok::Val(JsonValue::True)); + } + b'f' => { + i += 5; + stack.push(Tok::Val(JsonValue::False)); + } + b'{' => { + i += 1; + stack.push(Tok::StartObject); + } + b'}' => { + i += 1; + let mut values = HashMap::new(); + loop { + match stack.pop() { + None => bail!("unexpected object terminator"), + Some(Tok::StartObject) => break, + Some(Tok::StartArray) => { + bail!("unterminated array") + } + Some(Tok::Val(v)) => match stack.pop() { + None => bail!( + "unexpected object terminator (mismatch)" + ), + Some(Tok::StartObject) => { + bail!("mismatch item count") + } + Some(Tok::StartArray) => { + bail!("unterminated array") + } + Some(Tok::Val(JsonValue::String(k))) => { + values.insert(k, v); + } + Some(Tok::Val(_)) => { + bail!("object keys must be strings") + } + }, + } + } + stack.push(Tok::Val(JsonValue::Object(values))); + } + b'[' => { + i += 1; + stack.push(Tok::StartArray); + } + b']' => { + i += 1; + let mut values = Vec::new(); + loop { + match stack.pop() { + None => bail!("unexpected array terminator"), + Some(Tok::StartObject) => { + bail!("unterminated object") + } + Some(Tok::StartArray) => break, + Some(Tok::Val(v)) => values.push(v), + } + } + values.reverse(); + stack.push(Tok::Val(JsonValue::Array(values))); + } + b'"' => { + i += 1; + let start = i; + while i < blob.len() { + if blob[i] == b'"' { + break; + } + if blob[i] == b'\\' { + i += 1; + } + i += 1; + } + if i >= blob.len() { + bail!("Unterminated string at {i}"); + } + assert_eq!(blob[i], b'"'); + + let source = String::from_utf8_lossy(&blob[start..i]); + let mut chars = source.chars(); + i += 1; // Consume the final quote. + + let mut value = String::new(); + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next().expect("mismatched escape") { + '"' => value.push('"'), + '\\' => value.push('\\'), + '/' => value.push('/'), + 'b' => value.push('\x08'), + 'f' => value.push('\x0C'), + 'n' => value.push('\n'), + 'r' => value.push('\r'), + 't' => value.push('\t'), + 'u' => { + // 4 hex to a 16bit number. + // + // This is complicated because it might + // be the first part of a surrogate pair, + // which is a utf-16 thing that we should + // decode properly. (Hard to think of an + // acceptable way to cheat this.) So we + // buffer all codes we can find into a + // vector of u16s and then use + // std::char's `decode_utf16` to get the + // final string. We could do this with + // fewer allocations if we cared more. + let mut temp = String::with_capacity(4); + let mut utf16 = Vec::new(); + loop { + temp.clear(); + for _ in 0..4 { + let Some(c) = chars.next() else { + bail!("not enough chars in unicode escape") + }; + temp.push(c); + } + let code = + u16::from_str_radix(&temp, 16)?; + utf16.push(code); + + // `matches` only consumes on a match... + if !matches(&mut chars, "\\u") { + break; + } + } + + value.extend( + char::decode_utf16(utf16).map(|r| { + r.unwrap_or( + char::REPLACEMENT_CHARACTER, + ) + }), + ); + } + _ => bail!("Invalid json escape"), + } + } else { + value.push(c); + } + } + + stack.push(Tok::Val(JsonValue::String(value))); + } + b',' => i += 1, // Value separator in object or array + b':' => i += 1, // Key/Value separator + x if x.is_ascii_whitespace() => i += 1, + x if x == b'-' || x.is_ascii_digit() => { + let start = i; + while i < blob.len() { + match blob[i] { + b' ' | b'\t' | b'\r' | b'\n' | b'{' | b'}' + | b'[' | b']' | b',' | b':' => { + break; + } + _ => i += 1, + } + } + let number: f64 = + std::str::from_utf8(&blob[start..i])?.parse()?; + stack.push(Tok::Val(JsonValue::Number(number))); + } + + x => bail!("Invalid json value start byte {x}"), + } + } + + match stack.pop() { + Some(Tok::Val(v)) => Ok(v), + Some(Tok::StartObject) => bail!("unterminated object"), + Some(Tok::StartArray) => bail!("unterminated array"), + None => bail!("No JSON found in input"), + } + } + + pub fn as_array(&self) -> Option<&[JsonValue]> { + match self { + JsonValue::Array(v) => Some(v), + _ => None, + } + } + + pub fn as_object(&self) -> Option<&HashMap> { + match self { + JsonValue::Object(v) => Some(v), + _ => None, + } + } + + pub fn as_string(&self) -> Option<&str> { + match self { + JsonValue::String(v) => Some(v), + _ => None, + } + } + + pub fn as_number(&self) -> Option { + match self { + JsonValue::Number(f) => Some(*f), + _ => None, + } + } +} + +pub async fn get_entries() -> Result> { + let mut h: HashMap = HashMap::new(); + + let response = list_containers().await?; + let response = JsonValue::parse(&response)?; + let Some(containers) = response.as_array() else { + bail!("Expected an array of containers") + }; + for container in containers { + let Some(container) = container.as_object() else { + bail!("Expected containers to be objects"); + }; + + let name = container + .get("Names") + .and_then(|n| n.as_array()) + .and_then(|n| n.first()) + .and_then(|n| n.as_string()) + .unwrap_or(""); + + for port in container + .get("Ports") + .and_then(|n| n.as_array()) + .unwrap_or(&[]) + { + let Some(port) = port.as_object() else { + bail!("port records must be objects") + }; + if let Some(public_port) = + port.get("PublicPort").and_then(|pp| pp.as_number()) + { + // NOTE: If these are really ports then `as u16` will be + // right, otherwise what are we even doing here? + let public_port = public_port.trunc() as u16; + let private_port = port + .get("PrivatePort") + .and_then(|pp| pp.as_number()) + .unwrap_or(0.0) + .trunc() as u16; + + h.insert( + public_port, + PortDesc { + port: public_port, + desc: format!("{name} (docker->{private_port})"), + }, + ); + } + } + } + + Ok(h) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn json_decode_basic() { + let cases: Vec<(&str, JsonValue)> = vec![ + ("12", JsonValue::Number(12.0)), + ("12.7", JsonValue::Number(12.7)), + ("-12", JsonValue::Number(-12.0)), + ("true", JsonValue::True), + ("false", JsonValue::False), + ("null", JsonValue::Null), + ("\"abcd\"", JsonValue::String("abcd".to_string())), + ( + "\" a \\\" \\r b \\n c \\f d \\b e \\t f \\\\ \"", + JsonValue::String( + " a \" \r b \n c \x0C d \x08 e \t f \\ ".to_string(), + ), + ), + ]; + for (blob, expected) in cases { + assert_eq!(JsonValue::parse(blob.as_bytes()).unwrap(), expected); + } + } + + #[test] + pub fn json_decode_array() { + let result = JsonValue::parse(b"[1, true, \"foo\", null]").unwrap(); + let JsonValue::Array(result) = result else { + panic!("Expected an array"); + }; + assert_eq!(result.len(), 4); + assert_eq!(result[0], JsonValue::Number(1.0)); + assert_eq!(result[1], JsonValue::True); + assert_eq!(result[2], JsonValue::String("foo".to_owned())); + assert_eq!(result[3], JsonValue::Null); + } + + #[test] + pub fn json_decode_array_empty() { + let result = JsonValue::parse(b"[]").unwrap(); + assert_eq!(result, JsonValue::Array(vec![])); + } + + #[test] + pub fn json_decode_array_nested() { + let result = JsonValue::parse(b"[1, [2, 3], 4]").unwrap(); + assert_eq!( + result, + JsonValue::Array(vec![ + JsonValue::Number(1.0), + JsonValue::Array(vec![ + JsonValue::Number(2.0), + JsonValue::Number(3.0), + ]), + JsonValue::Number(4.0) + ]) + ); + } + + #[test] + pub fn json_decode_object() { + let result = JsonValue::parse( + b"{\"a\": 1.0, \"b\": [2.0, 3.0], \"c\": {\"d\": 4.0}}", + ) + .unwrap(); + assert_eq!( + result, + JsonValue::Object(HashMap::from([ + ("a".to_owned(), JsonValue::Number(1.0)), + ( + "b".to_owned(), + JsonValue::Array(vec![ + JsonValue::Number(2.0), + JsonValue::Number(3.0), + ]) + ), + ( + "c".to_owned(), + JsonValue::Object(HashMap::from([( + "d".to_owned(), + JsonValue::Number(4.0) + )])) + ) + ])) + ) + } + + #[test] + pub fn json_decode_test_files() { + use std::path::{Path, PathBuf}; + fn is_json(p: &Path) -> bool { + p.is_file() && p.extension().map(|s| s == "json").unwrap_or(false) + } + + let manifest_dir: PathBuf = + [env!("CARGO_MANIFEST_DIR"), "resources", "json"] + .iter() + .collect(); + + for file in manifest_dir.read_dir().unwrap().flatten() { + let path = file.path(); + if !is_json(&path) { + continue; + } + + let json = std::fs::read(&path).expect("Unable to read input file"); + let path = path.display(); + if let Err(err) = JsonValue::parse(&json) { + panic!("Unable to parse {path}: {err:?}"); + } + eprintln!("Parsed {path} successfully"); + } + } + + #[test] + pub fn json_decode_empty() { + assert!(JsonValue::parse(b" ").is_err()); + } + + #[test] + pub fn json_decode_docker() { + use pretty_assertions::assert_eq; + + // This is the example container response from docker + let result = JsonValue::parse(b" +[ + { + \"Id\": \"8dfafdbc3a40\", + \"Names\": [ + \"/boring_feynman\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 1\", + \"Created\": 1367854155, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [ + { + \"PrivatePort\": 2222, + \"PublicPort\": 3333, + \"Type\": \"tcp\" + } + ], + \"Labels\": { + \"com.example.vendor\": \"Acme\", + \"com.example.license\": \"GPL\", + \"com.example.version\": \"1.0\" + }, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.docker.type\": \"container\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"2cdc4edb1ded3631c81f57966563e5c8525b81121bb3706a9a9a3ae102711f3f\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.2\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:02\" + } + } + }, + \"Mounts\": [ + { + \"Name\": \"fac362...80535\", + \"Source\": \"/data\", + \"Destination\": \"/data\", + \"Driver\": \"local\", + \"Mode\": \"ro,Z\", + \"RW\": false, + \"Propagation\": \"\" + } + ] + }, + { + \"Id\": \"9cd87474be90\", + \"Names\": [ + \"/coolName\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 222222\", + \"Created\": 1367854155, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [], + \"Labels\": {}, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.docker.type\": \"container\", + \"io.kubernetes.sandbox.id\": \"3befe639bed0fd6afdd65fd1fa84506756f59360ec4adc270b0fdac9be22b4d3\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"88eaed7b37b38c2a3f0c4bc796494fdf51b270c2d22656412a2ca5d559a64d7a\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.8\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:08\" + } + } + }, + \"Mounts\": [] + }, + { + \"Id\": \"3176a2479c92\", + \"Names\": [ + \"/sleepy_dog\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 3333333333333333\", + \"Created\": 1367854154, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [], + \"Labels\": {}, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.image.id\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"io.kubernetes.image.name\": \"ubuntu:latest\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"8b27c041c30326d59cd6e6f510d4f8d1d570a228466f956edf7815508f78e30d\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.6\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:06\" + } + } + }, + \"Mounts\": [] + }, + { + \"Id\": \"4cb07b47f9fb\", + \"Names\": [ + \"/running_cat\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 444444444444444444444444444444444\", + \"Created\": 1367854152, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [], + \"Labels\": {}, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.config.source\": \"api\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"d91c7b2f0644403d7ef3095985ea0e2370325cd2332ff3a3225c4247328e66e9\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.5\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:05\" + } + } + }, + \"Mounts\": [] + } +] +").unwrap(); + let expected = JsonValue::Array(vec![ + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("8dfafdbc3a40".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/boring_feynman".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 1".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854155_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![ + JsonValue::Object(HashMap::from([ + ("PrivatePort".to_owned(), JsonValue::Number(2222_f64)), + ("PublicPort".to_owned(), JsonValue::Number(3333_f64)), + ("Type".to_owned(), JsonValue::String("tcp".to_owned())) + ])) + ])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([ + ("com.example.vendor".to_owned(), JsonValue::String("Acme".to_owned())), + ("com.example.license".to_owned(), JsonValue::String("GPL".to_owned())), + ("com.example.version".to_owned(), JsonValue::String("1.0".to_owned())) + ]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.docker.type".to_owned(), JsonValue::String("container".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("2cdc4edb1ded3631c81f57966563e5c8525b81121bb3706a9a9a3ae102711f3f".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.2".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:02".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![ + JsonValue::Object(HashMap::from([ + ("Name".to_owned(), JsonValue::String("fac362...80535".to_owned())), + ("Source".to_owned(), JsonValue::String("/data".to_owned())), + ("Destination".to_owned(), JsonValue::String("/data".to_owned())), + ("Driver".to_owned(), JsonValue::String("local".to_owned())), + ("Mode".to_owned(), JsonValue::String("ro,Z".to_owned())), + ("RW".to_owned(), JsonValue::False), + ("Propagation".to_owned(), JsonValue::String("".to_owned())) + ])) + ])) + ])), + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("9cd87474be90".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/coolName".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 222222".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854155_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.docker.type".to_owned(), JsonValue::String("container".to_owned())), + ("io.kubernetes.sandbox.id".to_owned(), JsonValue::String("3befe639bed0fd6afdd65fd1fa84506756f59360ec4adc270b0fdac9be22b4d3".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("88eaed7b37b38c2a3f0c4bc796494fdf51b270c2d22656412a2ca5d559a64d7a".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.8".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:08".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![])) + ])), + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("3176a2479c92".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/sleepy_dog".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 3333333333333333".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854154_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.image.id".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("io.kubernetes.image.name".to_owned(), JsonValue::String("ubuntu:latest".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("8b27c041c30326d59cd6e6f510d4f8d1d570a228466f956edf7815508f78e30d".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.6".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:06".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![])) + ])), + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("4cb07b47f9fb".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/running_cat".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 444444444444444444444444444444444".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854152_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.config.source".to_owned(), JsonValue::String("api".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("d91c7b2f0644403d7ef3095985ea0e2370325cd2332ff3a3225c4247328e66e9".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.5".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:05".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![])) + ])) + ]); + assert_eq!(result, expected); + } + + #[test] + pub fn json_decode_unterminated_string_with_escape() { + let input = b"\"\\"; + let _ = JsonValue::parse(input); + } + + async fn accept_and_send_single_response( + listener: tokio::net::TcpListener, + response: &[u8], + ) { + println!("[server] Awaiting connection..."); + let (stream, _) = listener + .accept() + .await + .expect("Unable to accept connection"); + let mut stream = tokio::io::BufStream::new(stream); + + println!("[server] Reading request..."); + let mut line = String::new(); + loop { + line.clear(); + stream + .read_line(&mut line) + .await + .expect("Unable to read line in server"); + if line.trim().is_empty() { + break; + } + } + + println!("[server] Sending response..."); + stream + .write_all(response) + .await + .expect("Unable to write response"); + stream.flush().await.expect("Unable to flush"); + println!("[server] Done."); + } + + #[tokio::test] + pub async fn docker_chunked_transfer_encoding() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("Unable to create listener on localhost"); + let port = listener.local_addr().unwrap().port(); + + let mut set = tokio::task::JoinSet::new(); + set.spawn(async move { + const RESPONSE: &[u8] = b"\ +HTTP/1.1 200 OK\r\n\ +Transfer-Encoding: chunked\r\n\ +\r\n\ +4\r\nWiki\r\n7\r\npedia i\r\nB\r\nn \r\nchunks.\r\n0\r\n\r\n"; + + accept_and_send_single_response(listener, RESPONSE).await; + }); + + let addr = format!("127.0.0.1:{port}"); + let stream = tokio::net::TcpStream::connect(&addr) + .await + .expect("Unable to connect"); + let response = list_containers_with_connection(stream) + .await + .expect("Unable to get response"); + assert_eq!(&response, b"Wikipedia in \r\nchunks."); + } + + #[tokio::test] + pub async fn docker_with_no_content_length() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("Unable to create listener on localhost"); + let port = listener.local_addr().unwrap().port(); + + let mut set = tokio::task::JoinSet::new(); + set.spawn(async move { + const RESPONSE: &[u8] = b"\ +HTTP/1.1 200 OK\r\n\ +\r\n\ +[\"Booo this is some data\"]\r\n"; + accept_and_send_single_response(listener, RESPONSE).await; + }); + + let addr = format!("127.0.0.1:{port}"); + let stream = tokio::net::TcpStream::connect(&addr) + .await + .expect("Unable to connect"); + let response = list_containers_with_connection(stream) + .await + .expect("Unable to get response"); + assert_eq!(&response, b"[\"Booo this is some data\"]\r\n"); + } +} diff --git a/src/server/refresh/procfs.rs b/src/server/refresh/procfs.rs new file mode 100644 index 0000000..9749dde --- /dev/null +++ b/src/server/refresh/procfs.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +use procfs::process::FDTarget; +use std::collections::HashMap; + +use crate::message::PortDesc; + +pub fn get_entries(send_anonymous: bool) -> Result> { + let all_procs = procfs::process::all_processes()?; + + // build up a map between socket inodes and process stat info. Ignore any + // error we encounter as it probably means we have no access to that + // process or something. + let mut map: HashMap = HashMap::new(); + for process in all_procs.flatten() { + if !process.is_alive() { + continue; // Ignore zombies. + } + + if let (Ok(fds), Ok(cmd)) = (process.fd(), process.cmdline()) { + for fd in fds.flatten() { + if let FDTarget::Socket(inode) = fd.target { + map.insert(inode, cmd.join(" ")); + } + } + } + } + + let mut h: HashMap = HashMap::new(); + + // Go through all the listening IPv4 and IPv6 sockets and take the first + // instance of listening on each port *if* the address is loopback or + // unspecified. (TODO: Do we want this restriction really?) + let tcp = procfs::net::tcp()?; + let tcp6 = procfs::net::tcp6()?; + for tcp_entry in tcp.into_iter().chain(tcp6) { + if tcp_entry.state == procfs::net::TcpState::Listen + && (tcp_entry.local_address.ip().is_loopback() + || tcp_entry.local_address.ip().is_unspecified()) + && !h.contains_key(&tcp_entry.local_address.port()) + { + // If the process is not one that we can identify, then we return + // the port but leave the description empty so that it can be + // identified by the client as "anonymous". + let desc = if let Some(cmd) = map.get(&tcp_entry.inode) { + cmd.clone() + } else { + String::new() + }; + + if send_anonymous || !desc.is_empty() { + h.insert( + tcp_entry.local_address.port(), + PortDesc { + port: tcp_entry.local_address.port(), + desc, + }, + ); + } + } + } + + Ok(h) +}