├── docker-compose.yml ├── Cargo.toml ├── Dockerfile ├── .gitignore ├── README.md ├── .github └── workflows │ ├── build.yml │ └── docker.yml ├── Cargo.lock └── src └── main.rs /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | netiso: 5 | image: ghcr.io/tuxuser/netiso-srv-rs:latest 6 | volumes: 7 | - /x360/games:/mnt 8 | ports: 9 | - 4323:4323 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "netiso-srv" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | binrw = "0.14.1" 8 | glob = "0.3.2" 9 | tokio = { version = "1", features = ["macros", "net", "io-util", "fs", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | WORKDIR /app 4 | COPY netiso-srv . 5 | RUN chmod +x ./netiso-srv 6 | 7 | VOLUME /mnt 8 | EXPOSE 4323/TCP 9 | 10 | # Quick smoke test 11 | RUN /app/netiso-srv -h 12 | 13 | CMD ["/app/netiso-srv", "-r", "/mnt"] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # RustRover 13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 15 | # and can be added to the global gitignore or merged into this file. For a more nuclear 16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 17 | #.idea/ 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/tuxuser/netiso-srv/actions/workflows/build.yml/badge.svg)](https://github.com/tuxuser/netiso-srv/actions/workflows/build.yml) 2 | [![Docker image tags](https://ghcr-badge.egpl.dev/tuxuser/netiso-srv-rs/tags?color=%2344cc11&ignore=latest&n=3&label=image+tags&trim=)](https://github.com/tuxuser/netiso-srv/pkgs/container/netiso-srv-rs) 3 | [![GitHub Release](https://img.shields.io/github/v/release/tuxuser/netiso-srv)](https://github.com/tuxuser/netiso-srv/releases/latest) 4 | 5 | # NetISO server daemon 6 | 7 | ## Usage 8 | 9 | Options: 10 | 11 | `-r` - Recursive scanning for ISO files 12 | `-v` - Verbose output 13 | `-h` - Print usage 14 | 15 | Run: `netiso-srv [-r] [-v] [-h] [directory with *.iso files]` 16 | 17 | 18 | ## Docker 19 | 20 | Spawn container standalone 21 | ``` 22 | docker run -p 4323:4323 -v /path/to/isos:/mnt ghcr.io/tuxuser/netiso-srv-rs:latest 23 | ``` 24 | 25 | or 26 | 27 | Spawn via docker compose 28 | ``` 29 | docker compose up 30 | ``` 31 | 32 | ## Development 33 | 34 | ### Build 35 | 36 | ``` 37 | cargo build [--release] 38 | ``` 39 | 40 | ### Build docker image locally 41 | 42 | Make sure to have the `netiso-srv` built and available in current directory. 43 | 44 | Build the image: 45 | 46 | ``` 47 | docker build -t netiso:localdev . 48 | ``` 49 | 50 | The resulting docker image is now ready-to-use from `netiso:localdev`, see `Docker`-steps above for regular docker-usage. 51 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | permissions: 8 | contents: write 9 | packages: write 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | target: [aarch64-linux-android, aarch64-unknown-linux-musl, arm-unknown-linux-musleabi, i686-pc-windows-gnu, x86_64-pc-windows-gnu, i686-unknown-linux-musl, x86_64-unknown-linux-musl] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - uses: dtolnay/rust-toolchain@master 24 | with: 25 | toolchain: stable 26 | 27 | - name: Install cross 28 | run: cargo install cross --git https://github.com/cross-rs/cross 29 | 30 | - name: Build ${{ matrix.target }} 31 | run: cross build --release --target ${{ matrix.target }} 32 | 33 | - name: Upload artifacts 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: netiso-srv-${{ matrix.target }} 37 | path: | 38 | README.md 39 | target/${{ matrix.target }}/release/netiso-srv* 40 | 41 | trigger_docker: 42 | name: Trigger docker build 43 | needs: [build] 44 | if: ${{ success() }} 45 | uses: ./.github/workflows/docker.yml 46 | with: 47 | workflow-run-id: "${{ github.run_id }}" 48 | 49 | release: 50 | name: Release 51 | runs-on: ubuntu-latest 52 | needs: [build] 53 | if: ${{ success() && startsWith(github.ref, 'refs/tags/') }} 54 | steps: 55 | - name: Download artifacts 56 | uses: actions/download-artifact@v4 57 | - name: Zip up builds 58 | run: for dir in *; do 7z a "${dir}.zip" "${dir}"; done 59 | - name: Create release 60 | uses: softprops/action-gh-release@v2 61 | with: 62 | files: | 63 | *.zip -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | workflow-run-id: 7 | required: true 8 | type: string 9 | 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | env: 15 | REGISTRY_IMAGE: ghcr.io/tuxuser/netiso-srv-rs 16 | 17 | jobs: 18 | build-docker: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | arch: [aarch64-unknown-linux-musl, arm-unknown-linux-musleabi, i686-unknown-linux-musl, x86_64-unknown-linux-musl] 24 | include: 25 | - arch: aarch64-unknown-linux-musl 26 | buildx-platform: linux/arm64 27 | - arch: arm-unknown-linux-musleabi 28 | buildx-platform: linux/arm/v7 29 | - arch: x86_64-unknown-linux-musl 30 | buildx-platform: linux/amd64 31 | - arch: i686-unknown-linux-musl 32 | buildx-platform: linux/386 33 | 34 | steps: 35 | - name: Prepare 36 | run: | 37 | platform=${{ matrix.buildx-platform }} 38 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 39 | 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | - name: Download artifact 43 | uses: actions/download-artifact@v4 44 | with: 45 | name: netiso-srv-${{ matrix.arch }} 46 | github-token: ${{ github.token }} 47 | repository: ${{ github.repository }} 48 | run-id: ${{ inputs.workflow-run-id }} 49 | 50 | # https://github.com/docker/setup-qemu-action 51 | - name: Set up QEMU 52 | uses: docker/setup-qemu-action@v3 53 | # https://github.com/docker/setup-buildx-action 54 | - name: Set up Docker Buildx 55 | id: buildx 56 | uses: docker/setup-buildx-action@v3 57 | 58 | - name: Available platforms 59 | run: echo ${{ steps.buildx.outputs.platforms }} 60 | 61 | - name: Login to GHCR 62 | if: github.event_name != 'pull_request' 63 | uses: docker/login-action@v3 64 | with: 65 | registry: ghcr.io 66 | username: ${{ github.repository_owner }} 67 | password: ${{ secrets.GITHUB_TOKEN }} 68 | 69 | #- name: Login to Docker Hub 70 | # if: github.event_name != 'pull_request' 71 | # uses: docker/login-action@v3 72 | # with: 73 | # username: ${{ secrets.DOCKERHUB_USERNAME }} 74 | # password: ${{ secrets.DOCKERHUB_TOKEN }} 75 | 76 | - name: Docker metadata 77 | id: docker_meta 78 | uses: docker/metadata-action@v5 79 | with: 80 | # list of Docker images to use as base name for tags 81 | images: ${{ env.REGISTRY_IMAGE }} 82 | 83 | - name: Copy binary 84 | run: | 85 | cp ./target/${{ matrix.arch }}/release/netiso-srv . 86 | 87 | - name: Build and push 88 | id: build 89 | uses: docker/build-push-action@v6 90 | with: 91 | context: . 92 | file: ./Dockerfile 93 | platforms: ${{ matrix.buildx-platform }} 94 | push: ${{ github.event_name != 'pull_request' }} 95 | tags: ${{ env.REGISTRY_IMAGE }} 96 | labels: ${{ steps.docker_meta.outputs.labels }} 97 | outputs: type=image,push-by-digest=true,name-canonical=true,push=true 98 | 99 | - name: Export digest 100 | run: | 101 | mkdir -p ${{ runner.temp }}/digests 102 | digest="${{ steps.build.outputs.digest }}" 103 | touch "${{ runner.temp }}/digests/${digest#sha256:}" 104 | 105 | - name: Upload digest 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: digests-${{ env.PLATFORM_PAIR }} 109 | path: ${{ runner.temp }}/digests/* 110 | if-no-files-found: error 111 | retention-days: 1 112 | 113 | # - name: Docker Hub Description 114 | # if: github.event_name != 'pull_request' 115 | # uses: peter-evans/dockerhub-description@v4 116 | # with: 117 | # username: ${{ secrets.DOCKERHUB_USERNAME }} 118 | # password: ${{ secrets.DOCKERHUB_PASSWORD }} 119 | # repository: tuxuser/netiso-srv-rs 120 | # readme-filepath: ./README.md 121 | # short-description: "Alternative NetISO server for x360 netiso dashlaunch plugin" 122 | 123 | merge: 124 | runs-on: ubuntu-latest 125 | needs: [build-docker] 126 | steps: 127 | - name: Download digests 128 | uses: actions/download-artifact@v4 129 | with: 130 | path: ${{ runner.temp }}/digests 131 | pattern: digests-* 132 | merge-multiple: true 133 | 134 | - name: Login to GHCR 135 | if: github.event_name != 'pull_request' 136 | uses: docker/login-action@v3 137 | with: 138 | registry: ghcr.io 139 | username: ${{ github.repository_owner }} 140 | password: ${{ secrets.GITHUB_TOKEN }} 141 | 142 | - name: Set up Docker Buildx 143 | uses: docker/setup-buildx-action@v3 144 | 145 | - name: Docker metadata 146 | id: docker_meta 147 | uses: docker/metadata-action@v5 148 | with: 149 | # list of Docker images to use as base name for tags 150 | images: ${{ env.REGISTRY_IMAGE }} 151 | # generate Docker tags based on the following events/attributes 152 | tags: | 153 | type=schedule 154 | type=ref,event=branch 155 | type=ref,event=pr 156 | type=semver,pattern={{version}} 157 | type=semver,pattern={{major}}.{{minor}} 158 | type=semver,pattern={{major}} 159 | type=sha 160 | 161 | - name: Create manifest list and push 162 | if: github.event_name != 'pull_request' 163 | working-directory: ${{ runner.temp }}/digests 164 | run: | 165 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 166 | $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) 167 | 168 | - name: Inspect image 169 | run: | 170 | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.docker_meta.outputs.version }} -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "array-init" 22 | version = "2.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" 25 | 26 | [[package]] 27 | name = "backtrace" 28 | version = "0.3.74" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 31 | dependencies = [ 32 | "addr2line", 33 | "cfg-if", 34 | "libc", 35 | "miniz_oxide", 36 | "object", 37 | "rustc-demangle", 38 | "windows-targets", 39 | ] 40 | 41 | [[package]] 42 | name = "binrw" 43 | version = "0.14.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" 46 | dependencies = [ 47 | "array-init", 48 | "binrw_derive", 49 | "bytemuck", 50 | ] 51 | 52 | [[package]] 53 | name = "binrw_derive" 54 | version = "0.14.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c" 57 | dependencies = [ 58 | "either", 59 | "owo-colors", 60 | "proc-macro2", 61 | "quote", 62 | "syn 1.0.109", 63 | ] 64 | 65 | [[package]] 66 | name = "bytemuck" 67 | version = "1.19.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" 70 | 71 | [[package]] 72 | name = "bytes" 73 | version = "1.10.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 76 | 77 | [[package]] 78 | name = "cfg-if" 79 | version = "1.0.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 82 | 83 | [[package]] 84 | name = "either" 85 | version = "1.13.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 88 | 89 | [[package]] 90 | name = "gimli" 91 | version = "0.31.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 94 | 95 | [[package]] 96 | name = "glob" 97 | version = "0.3.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 100 | 101 | [[package]] 102 | name = "libc" 103 | version = "0.2.161" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 106 | 107 | [[package]] 108 | name = "memchr" 109 | version = "2.7.4" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 112 | 113 | [[package]] 114 | name = "miniz_oxide" 115 | version = "0.8.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 118 | dependencies = [ 119 | "adler2", 120 | ] 121 | 122 | [[package]] 123 | name = "mio" 124 | version = "1.0.3" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 127 | dependencies = [ 128 | "libc", 129 | "wasi", 130 | "windows-sys", 131 | ] 132 | 133 | [[package]] 134 | name = "netiso-srv" 135 | version = "0.1.0" 136 | dependencies = [ 137 | "binrw", 138 | "glob", 139 | "tokio", 140 | ] 141 | 142 | [[package]] 143 | name = "object" 144 | version = "0.36.5" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 147 | dependencies = [ 148 | "memchr", 149 | ] 150 | 151 | [[package]] 152 | name = "owo-colors" 153 | version = "3.5.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 156 | 157 | [[package]] 158 | name = "pin-project-lite" 159 | version = "0.2.15" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 162 | 163 | [[package]] 164 | name = "proc-macro2" 165 | version = "1.0.89" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 168 | dependencies = [ 169 | "unicode-ident", 170 | ] 171 | 172 | [[package]] 173 | name = "quote" 174 | version = "1.0.37" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 177 | dependencies = [ 178 | "proc-macro2", 179 | ] 180 | 181 | [[package]] 182 | name = "rustc-demangle" 183 | version = "0.1.24" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 186 | 187 | [[package]] 188 | name = "socket2" 189 | version = "0.5.8" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 192 | dependencies = [ 193 | "libc", 194 | "windows-sys", 195 | ] 196 | 197 | [[package]] 198 | name = "syn" 199 | version = "1.0.109" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 202 | dependencies = [ 203 | "proc-macro2", 204 | "quote", 205 | "unicode-ident", 206 | ] 207 | 208 | [[package]] 209 | name = "syn" 210 | version = "2.0.85" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 213 | dependencies = [ 214 | "proc-macro2", 215 | "quote", 216 | "unicode-ident", 217 | ] 218 | 219 | [[package]] 220 | name = "tokio" 221 | version = "1.41.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" 224 | dependencies = [ 225 | "backtrace", 226 | "bytes", 227 | "libc", 228 | "mio", 229 | "pin-project-lite", 230 | "socket2", 231 | "tokio-macros", 232 | "windows-sys", 233 | ] 234 | 235 | [[package]] 236 | name = "tokio-macros" 237 | version = "2.4.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 240 | dependencies = [ 241 | "proc-macro2", 242 | "quote", 243 | "syn 2.0.85", 244 | ] 245 | 246 | [[package]] 247 | name = "unicode-ident" 248 | version = "1.0.13" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 251 | 252 | [[package]] 253 | name = "wasi" 254 | version = "0.11.0+wasi-snapshot-preview1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 257 | 258 | [[package]] 259 | name = "windows-sys" 260 | version = "0.52.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 263 | dependencies = [ 264 | "windows-targets", 265 | ] 266 | 267 | [[package]] 268 | name = "windows-targets" 269 | version = "0.52.6" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 272 | dependencies = [ 273 | "windows_aarch64_gnullvm", 274 | "windows_aarch64_msvc", 275 | "windows_i686_gnu", 276 | "windows_i686_gnullvm", 277 | "windows_i686_msvc", 278 | "windows_x86_64_gnu", 279 | "windows_x86_64_gnullvm", 280 | "windows_x86_64_msvc", 281 | ] 282 | 283 | [[package]] 284 | name = "windows_aarch64_gnullvm" 285 | version = "0.52.6" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 288 | 289 | [[package]] 290 | name = "windows_aarch64_msvc" 291 | version = "0.52.6" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 294 | 295 | [[package]] 296 | name = "windows_i686_gnu" 297 | version = "0.52.6" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 300 | 301 | [[package]] 302 | name = "windows_i686_gnullvm" 303 | version = "0.52.6" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 306 | 307 | [[package]] 308 | name = "windows_i686_msvc" 309 | version = "0.52.6" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 312 | 313 | [[package]] 314 | name = "windows_x86_64_gnu" 315 | version = "0.52.6" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 318 | 319 | [[package]] 320 | name = "windows_x86_64_gnullvm" 321 | version = "0.52.6" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 324 | 325 | [[package]] 326 | name = "windows_x86_64_msvc" 327 | version = "0.52.6" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 330 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::upper_case_acronyms)] 2 | 3 | use binrw::{BinRead, BinWrite}; 4 | use glob::glob; 5 | use tokio::fs::File; 6 | use std::env; 7 | use std::error::Error; 8 | use std::ffi::OsStr; 9 | use std::io::Cursor; 10 | use std::path::{Path, PathBuf}; 11 | use tokio::io::{AsyncReadExt, AsyncSeekExt}; 12 | use tokio::net::TcpListener; 13 | 14 | const NETISO_SRV_PORT: u16 = 4323; 15 | const SECTOR_SIZE: u16 = 0x800; // 2048 16 | const XGD_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA"; 17 | 18 | #[derive(Debug)] 19 | enum IsoType { 20 | XSF, 21 | XGD2, 22 | XGD3 23 | } 24 | 25 | #[derive(Debug)] 26 | struct ActiveIso { 27 | file: File, 28 | metadata: IsoEntry, 29 | } 30 | 31 | #[derive(Clone, Debug)] 32 | struct IsoEntry { 33 | path: PathBuf, 34 | filename: String, 35 | filesize: u64, 36 | data_start: u64, 37 | sector_count: u64, 38 | has_type1_file: u32, 39 | } 40 | 41 | #[derive(BinRead, BinWrite, Debug)] 42 | #[brw(repr = u16)] 43 | enum Cmd { 44 | Ping = 0, 45 | GetIsoSize = 1, 46 | HasType1File = 2, 47 | ReadData = 3, 48 | GetIsoName = 4, 49 | MountIso = 5, 50 | } 51 | 52 | // Message structure definition 53 | #[derive(BinRead, BinWrite, Debug)] 54 | #[brw(big, magic = b"ISVR")] 55 | struct Message { 56 | cmd_type: Cmd, 57 | iso_index: u16, 58 | offset: u64, 59 | length: u32, 60 | } 61 | 62 | #[derive(Default, Debug)] 63 | struct Server { 64 | files: Vec, 65 | active_file: Option, 66 | verbose: bool, 67 | } 68 | 69 | async fn get_data_start(file: &mut File) -> Result> { 70 | let offsets: [(u64, u64); 3] = [ 71 | // XGD2 / GDF 72 | (0xfda0000, 0xfd90000), 73 | // XGD3 74 | (0x2090000, 0x2080000), 75 | // XSF 76 | (0x10000, 0x0) 77 | ]; 78 | 79 | let len = file.metadata().await?.len(); 80 | let mut buf = vec![0u8; XGD_MAGIC.len()]; 81 | for (offset, data_start) in offsets { 82 | if len >= offset + XGD_MAGIC.len() as u64 { 83 | file.seek(std::io::SeekFrom::Start(offset)).await?; 84 | file.read_exact(&mut buf).await?; 85 | 86 | if buf == XGD_MAGIC { 87 | return Ok(data_start); 88 | } 89 | } 90 | } 91 | 92 | // No XGD Magic found in expected offsets 93 | // Assume data starts @ 0x0 94 | Ok(0) 95 | } 96 | 97 | async fn get_iso_files(old_entries: &Vec, directory: &Path, recursive: bool) -> Result, Box> { 98 | let mut ret = old_entries.clone(); 99 | 100 | // First, throw out obsolete entries 101 | ret.retain(|x| x.path.exists()); 102 | 103 | // Assemble glob pattern 104 | let isofiles_glob_pattern = { 105 | let mut path_glob = directory.to_str().unwrap().to_string(); 106 | if recursive { 107 | path_glob += std::path::MAIN_SEPARATOR_STR; 108 | path_glob += "**" 109 | } 110 | 111 | path_glob += std::path::MAIN_SEPARATOR_STR; 112 | path_glob += "*.iso"; 113 | 114 | path_glob 115 | }; 116 | 117 | // Search for new files 118 | let files: Vec = glob(&isofiles_glob_pattern)? 119 | .filter_map(|x| x.ok()) 120 | // Filter for existing files 121 | .filter(|x|x.is_file()) 122 | // Filter for new files (PathBuf not identical to any previous entry) 123 | .filter(|x| 124 | ret 125 | .iter() 126 | .find(|y|y.path == *x) 127 | .is_none() 128 | ) 129 | .collect(); 130 | 131 | for filepath in files { 132 | let filesize = filepath.metadata()?.len(); 133 | let filename = filepath.file_name() 134 | .unwrap_or(OsStr::new("")) 135 | .to_str() 136 | .unwrap_or("") 137 | .to_string(); 138 | let mut handle = File::open(&filepath).await?; 139 | let data_start = match get_data_start(&mut handle).await { 140 | Ok(data_start) => data_start, 141 | Err(err) => { 142 | eprintln!("Invalid iso file: {filepath:?}, err: {err:?}"); 143 | continue; 144 | } 145 | }; 146 | 147 | let entry = IsoEntry { 148 | path: filepath.clone(), 149 | filename, 150 | filesize, 151 | data_start, 152 | sector_count: filesize / SECTOR_SIZE as u64, 153 | has_type1_file: 0, 154 | }; 155 | 156 | ret.push(entry); 157 | } 158 | 159 | Ok(ret) 160 | } 161 | 162 | async fn scan_iso_files_initial(directory: &Path, recursive: bool) -> Result, Box> { 163 | get_iso_files(&vec![], directory, recursive).await 164 | } 165 | 166 | impl Server { 167 | async fn disable_current_iso(&mut self) { 168 | if self.active_file.is_some() { 169 | self.active_file = None; 170 | } 171 | } 172 | 173 | async fn handler(&mut self, mut socket: tokio::net::TcpStream) -> Result<(), Box> { 174 | loop { 175 | let mut buffer = [0; 20]; 176 | match socket.read(&mut buffer).await { 177 | Ok(size) => { 178 | if size == 0 { 179 | eprintln!("EOF - Client '{:?}' disconnected", socket.peer_addr()); 180 | self.disable_current_iso().await; 181 | break 182 | } 183 | 184 | let mut cur = Cursor::new(&buffer); 185 | let msg = Message::read(&mut cur)?; 186 | 187 | if self.verbose { 188 | println!("< {msg:?}"); 189 | } 190 | 191 | match msg.cmd_type { 192 | Cmd::Ping => { 193 | let reply = "ISVRokOK".as_bytes(); 194 | socket.try_write(reply)?; 195 | }, 196 | Cmd::GetIsoSize => { 197 | // Get iso sector count for mounted file 198 | // If no iso is mounted, reply with 0 199 | let sector_count = match &self.active_file { 200 | Some(iso) => { 201 | iso.metadata.sector_count 202 | }, 203 | None => 0, 204 | }; 205 | 206 | let mut resp = vec![]; 207 | resp.extend_from_slice(&(sector_count as u32).to_be_bytes()); 208 | resp.extend_from_slice(&(SECTOR_SIZE as u32).to_be_bytes()); 209 | 210 | socket.try_write(&resp)?; 211 | }, 212 | Cmd::HasType1File => { 213 | let maybe_iso = self.files.get(msg.iso_index as usize); 214 | let has_type1_file = match maybe_iso { 215 | Some(iso) => { 216 | iso.has_type1_file 217 | }, 218 | None => { 219 | 0 220 | } 221 | }; 222 | 223 | socket.try_write(&has_type1_file.to_be_bytes())?; 224 | }, 225 | Cmd::ReadData => { 226 | if let Some(active) = self.active_file.as_mut() { 227 | let mut buf = vec![0u8; msg.length as usize]; 228 | 229 | active.file.seek(std::io::SeekFrom::Start(msg.offset)).await?; 230 | active.file.read_exact(&mut buf).await?; 231 | 232 | socket.try_write(&buf)?; 233 | } 234 | }, 235 | Cmd::GetIsoName => { 236 | let maybe_iso = self.files.get(msg.iso_index as usize); 237 | let filename = match maybe_iso { 238 | Some(iso) => { 239 | &iso.filename 240 | }, 241 | None => { 242 | "" 243 | } 244 | }; 245 | let mut response = filename.as_bytes().to_vec(); 246 | // The request contains the expected bytecount, so we extend the slice here 247 | response.resize(msg.length as usize, 0); 248 | 249 | socket.try_write(&response)?; 250 | }, 251 | Cmd::MountIso => { 252 | let mut iso_name = vec![0; msg.length as usize]; 253 | assert_eq!(socket.read(&mut iso_name).await?, msg.length as usize); 254 | 255 | let iso_name_human = String::from_utf8(iso_name)?; 256 | 257 | let normalized = iso_name_human 258 | .replace("\\Mount", "") 259 | .replace("\\", "") 260 | .replace("\x00", ""); 261 | 262 | println!("Normalized ISO Name: {iso_name_human} -> {normalized}"); 263 | 264 | if normalized == "[Disable Current ISO]" { 265 | println!("Unmounting current iso..."); 266 | self.disable_current_iso().await; 267 | let code = 0u32; 268 | socket.try_write(&code.to_be_bytes())?; 269 | } else { 270 | let found = self.files.iter().find(|x| x.filename.ends_with(&normalized)); 271 | 272 | let code: u32 = match found { 273 | Some(iso) => { 274 | println!("Mounting: {:?}", iso.path); 275 | let file = File::open(&iso.path).await?; 276 | self.active_file = Some(ActiveIso { file: file, metadata: iso.to_owned() }); 277 | 1 // success 278 | }, 279 | None => { 280 | eprintln!("MountIso: Failed to find ISO '{normalized}' !"); 281 | 0 // error 282 | } 283 | }; 284 | 285 | socket.try_write(&code.to_be_bytes())?; 286 | } 287 | } 288 | } 289 | }, 290 | Err(err) => { 291 | eprintln!("Failed reading from socket, err: {err}"); 292 | } 293 | } 294 | } 295 | 296 | Ok(()) 297 | } 298 | 299 | async fn handle_connection(&mut self, socket: tokio::net::TcpStream) { 300 | if let Err(err) = self.handler(socket).await { 301 | eprintln!("Connection handler exited with error: {err}"); 302 | } 303 | } 304 | } 305 | 306 | fn print_usage(bin_name: &str) { 307 | eprintln!("Usage: {} [-rbvh] [iso directory path]", bin_name); 308 | eprintln!("\nArgs:"); 309 | eprintln!("\t-r - Recursive ISO scanning"); 310 | eprintln!("\t-b - Enable workaround for big iso library (132+ games)"); 311 | eprintln!("\t-v - Verbose output"); 312 | eprintln!("\t-h - Print help / usage") 313 | } 314 | 315 | fn check_arg(args: &mut Vec, arg_name: &str) -> bool { 316 | match args.iter().position(|x| arg_name == x) { 317 | Some(removal_index) => { 318 | args.remove(removal_index); 319 | true 320 | }, 321 | None => false 322 | } 323 | } 324 | 325 | #[tokio::main] 326 | async fn main() -> Result<(), Box> { 327 | let mut args: Vec = env::args().collect(); 328 | 329 | let print_help = check_arg(&mut args, "-h"); // Help 330 | let recursive_scan = check_arg(&mut args, "-r"); // Recursive iso scanning 331 | let verbose = check_arg(&mut args, "-v"); // Verbose / Debug 332 | 333 | if print_help { 334 | print_usage(&args[0]); 335 | return Ok(()); 336 | } 337 | else if args.len() < 2 { 338 | eprintln!("ERROR: Invalid number of arguments!"); 339 | print_usage(&args[0]); 340 | return Ok(()); 341 | } 342 | 343 | let filepath = Path::new(&args[1]); 344 | 345 | println!("Enumerating ISOs in {filepath:?}..."); 346 | let mut files = scan_iso_files_initial(filepath, recursive_scan).await?; 347 | 348 | if files.is_empty() { 349 | return Err("No iso files enumerated".into()); 350 | } 351 | 352 | println!("Found the following ISOs"); 353 | for (index, file) in files.iter().enumerate() { 354 | println!("{index}: {}", &file.filename); 355 | } 356 | 357 | let listener = TcpListener::bind(("0.0.0.0", NETISO_SRV_PORT)).await?; 358 | println!("Start listening for incoming connections..."); 359 | 360 | loop { 361 | let (socket, _) = listener.accept().await?; 362 | println!("Got connection from: {:?}", &socket.peer_addr()); 363 | 364 | // Update list of isos 365 | files = get_iso_files(&files, filepath, recursive_scan).await?; 366 | 367 | let files_clone = files.clone(); 368 | tokio::spawn(async move { 369 | let mut srv = Server { 370 | files: files_clone, 371 | verbose: verbose, 372 | ..Default::default() 373 | }; 374 | srv.handle_connection(socket).await 375 | }); 376 | } 377 | } 378 | --------------------------------------------------------------------------------