Compare commits

...

10 commits

Author SHA1 Message Date
4df87e3b84 Add flake.nix and fix test that only passed in debug builds
Some checks failed
CI / build (push) Has been cancelled
CI / check (push) Has been cancelled
CI / test (macos-latest) (push) Has been cancelled
CI / test (ubuntu-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
CI / docs (push) Has been cancelled
- Add flake.nix with package (including examples) and dev shell
- Track Cargo.lock (removed from .gitignore) as required by the nix build
- Change debug_assert! to assert! in UsbIpResponse::to_bytes() so
  invariant checks fire in release mode, fixing the
  byte_serialize_invalid_usbip_ret_submit #[should_panic] test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:55:15 +00:00
Jiajie Chen
0878920532 Release v0.8.0 2026-01-27 13:00:46 +08:00
Jiajie Chen
a19bfe8301 Simplify documentation (fixes #59)
Remove redundant sections from README and update LICENSE copyright year.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-12-25 20:58:48 +08:00
Jiajie Chen
c8cd214e1d Fix clippy warning: cloned_ref_to_slice_refs
Replace &[device.clone()] with std::slice::from_ref(&device) in
test to address clippy::cloned_ref_to_slice_refs warning.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-12-25 20:52:45 +08:00
Jiajie Chen
48d0d9ca34 Fix bus num on non-Linux platform 2025-12-24 23:23:25 +08:00
Jiajie Chen
b83f584391 Handle upper case in hid keyboard report 2025-12-24 23:18:11 +08:00
Jiajie Chen
fdbb9574aa Fix missing max packet size handling 2025-12-24 23:17:29 +08:00
Jiajie Chen
3ff5df5c2e Migrate to nusb 0.2.1 2025-12-24 23:17:02 +08:00
Jiajie Chen
156ec45a81 Apply more cargo clippy 2025-08-01 16:38:47 +08:00
Jiajie Chen
e14858f918 Apply cargo clippy 2025-08-01 16:35:06 +08:00
13 changed files with 991 additions and 135 deletions

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
/target
Cargo.lock
/result

614
Cargo.lock generated Normal file
View file

@ -0,0 +1,614 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "env_filter"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "io-kit-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06d3a048d09fbb6597dbf7c69f40d14df4a49487db1487191618c893fc3b1c26"
dependencies = [
"core-foundation-sys",
"mach2",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "jiff"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "libc"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libusb1-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "mach2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [
"libc",
]
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "nusb"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a330b3bc7f8b4fc729a4c63164b3927eeeaced198222a3ce6b8b6e034851b7a"
dependencies = [
"core-foundation",
"core-foundation-sys",
"futures-core",
"io-kit-sys",
"linux-raw-sys",
"log",
"once_cell",
"rustix",
"slab",
"windows-sys",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rusb"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4"
dependencies = [
"libc",
"libusb1-sys",
"serde",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[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.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "usbip"
version = "0.8.0"
dependencies = [
"env_logger",
"log",
"num-derive",
"num-traits",
"nusb",
"rusb",
"serde",
"tokio",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]

View file

@ -1,6 +1,6 @@
[package]
name = "usbip"
version = "0.7.1"
version = "0.8.0"
authors = ["Jiajie Chen <c@jia.je>"]
edition = "2024"
license = "MIT"
@ -16,7 +16,7 @@ num-traits = "0.2.15"
num-derive = "0.4.2"
rusb = "0.9.3"
serde = { version = "1.0", features = ["derive"], optional = true }
nusb = "0.1.10"
nusb = "0.2.1"
[dev-dependencies]
tokio = { version = "1.22.0", features = ["full"] }

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Jiajie Chen
Copyright (c) 2020-2025 Jiajie Chen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View file

@ -3,33 +3,58 @@
[![Coverage Status](https://coveralls.io/repos/github/jiegec/usbip/badge.svg?branch=master)](https://coveralls.io/github/jiegec/usbip?branch=master)
[![crates.io](https://img.shields.io/crates/v/usbip.svg)](https://crates.io/crates/usbip)
A Rust library to run a USB/IP server to simulate USB devices.
A Rust library to run a USB/IP server to simulate USB devices and share real USB devices over a network.
It also enables sharing devices from an OS supporting libusb(libusb claims that it supports Linux, macOS, Windows, OpenBSD/NetBSD, Haiku and Solaris) to another OS supporting USB/IP(Linux, Windows). Sharing an CCID SmartCard from macOS to Linux is tested by running `gpg --card-status`.
## What is USB/IP?
USB/IP is a network protocol that allows USB devices to be shared between computers over a network. It enables:
- **Device simulation**: Create virtual USB devices that can be accessed remotely
- **Device sharing**: Share physical USB devices from one machine to another
- **Cross-platform**: Works across different operating systems (Linux, etc.)
## Installation
### Prerequisites
Install Rust from the [official documentation](https://www.rust-lang.org/tools/install).
### Building from source
```bash
git clone https://github.com/jiegec/usbip.git
cd usbip
cargo build --release
```
## How to use
See examples directory. Three examples are provided:
### Examples
1. hid_keyboard: Simulate a hid keyboard that types something every second.
2. cdc_acm_serial: Simulate a serial that gets a character every second.
3. host: Act like original usb/ip sharing server, sharing one device from one machine to another. Also supports sharing from macOS to Linux!
The `examples/` directory contains three example programs:
To run example, run:
1. **hid_keyboard**: Simulate a HID keyboard that types something every second
2. **cdc_acm_serial**: Simulate a CDC ACM serial device that receives a character every second
3. **host**: Act as a USB/IP server, sharing physical devices from the host machine to remote clients
#### Running an example
```bash
$ env RUST_LOG=info cargo run --example hid_keyboard
cargo run --example hid_keyboard
```
Then, in a USB/IP client environment:
#### Connecting from a USB/IP client
On the client machine (e.g. Linux with USB/IP support):
```bash
$ usbip list -r $remote_ip
$ usbip attach -r $remote_ip -b $bus_id
# List available devices
usbip list -r $remote_ip
# Attach to a device
usbip attach -r $remote_ip -b $bus_id
```
Then, you can inspect the simulated USB device behavior in both sides.
## License
## API
See code comments. Not finalized yet, so get prepared for api breaking changes.
MIT License - see [LICENSE](LICENSE) file for details.

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1773628058,
"narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

65
flake.nix Normal file
View file

@ -0,0 +1,65 @@
{
description = "A library to run USB/IP server";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
nativeBuildInputs = with pkgs; [
rustc
cargo
pkg-config
];
buildInputs = with pkgs; [
libusb1
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
udev
];
usbip = pkgs.rustPlatform.buildRustPackage {
pname = "usbip";
version = "0.8.0";
src = self;
cargoHash = "sha256-PiJvE9CWbNIZif/ku3G+A7g5vSzl2O80a33NZdgmFL4=";
inherit nativeBuildInputs buildInputs;
buildFeatures = [ "serde" ];
# Build both the library and all examples
cargoBuildFlags = [ "--examples" ];
postInstall = ''
for f in target/*/release/examples/{hid_keyboard,cdc_acm_serial,host}; do
[ -f "$f" ] && install -Dm755 "$f" "$out/bin/$(basename $f)"
done
'';
meta = with pkgs.lib; {
description = "A library to run USB/IP server";
homepage = "https://github.com/jiegec/usbip";
license = licenses.mit;
mainProgram = "host";
};
};
in
{
packages = {
default = usbip;
inherit usbip;
};
devShells.default = pkgs.mkShell {
inherit nativeBuildInputs buildInputs;
};
});
}

View file

@ -75,9 +75,17 @@ impl UsbInterfaceHandler for UsbCdcAcmHandler {
return Ok(vec![]);
} else {
// bulk in
// TODO: handle max packet size
let resp = self.tx_buffer.clone();
self.tx_buffer.clear();
// Handle max packet size - return data in chunks of max_packet_size
let max_packet_size = ep.max_packet_size as usize;
let resp = if self.tx_buffer.len() > max_packet_size {
// Return only the first chunk (max_packet_size bytes)
self.tx_buffer.drain(..max_packet_size).collect::<Vec<_>>()
} else {
// Return all data if it fits in one packet
let resp = self.tx_buffer.clone();
self.tx_buffer.clear();
resp
};
return Ok(resp);
}
}

View file

@ -291,7 +291,7 @@ impl UsbDevice {
match (FromPrimitive::from_u8(ep.attributes), ep.direction()) {
(Some(Control), In) => {
// control in
debug!("Control IN setup={:x?}", setup_packet);
debug!("Control IN setup={setup_packet:x?}");
match (
setup_packet.request_type,
FromPrimitive::from_u8(setup_packet.request),
@ -436,7 +436,7 @@ impl UsbDevice {
} else {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid string index: {}", index),
format!("Invalid string index: {index}"),
))
}
}
@ -463,7 +463,7 @@ impl UsbDevice {
Ok(desc)
}
_ => {
warn!("unknown desc type: {:x?}", setup_packet);
warn!("unknown desc type: {setup_packet:x?}");
Ok(vec![])
}
}
@ -488,7 +488,7 @@ impl UsbDevice {
}
(Some(Control), Out) => {
// control out
debug!("Control OUT setup={:x?}", setup_packet);
debug!("Control OUT setup={setup_packet:x?}");
match (
setup_packet.request_type,
FromPrimitive::from_u8(setup_packet.request),

View file

@ -35,16 +35,17 @@ pub struct UsbHidKeyboardReport {
impl UsbHidKeyboardReport {
pub fn from_ascii(ascii: u8) -> UsbHidKeyboardReport {
// TODO: casing
let key = match ascii {
b'a'..=b'z' => ascii - b'a' + 4,
b'1'..=b'9' => ascii - b'1' + 30,
b'0' => 39,
b'\r' | b'\n' => 40,
let (modifier, key) = match ascii {
b'a'..=b'z' => (0, ascii - b'a' + 4),
b'A'..=b'Z' => (0x02, ascii - b'A' + 4), // Left Shift modifier
b'1'..=b'9' => (0, ascii - b'1' + 30),
b'0' => (0, 39),
b'\r' | b'\n' => (0, 40),
b' ' => (0, 44), // Space
_ => unimplemented!("Unrecognized ascii {}", ascii),
};
UsbHidKeyboardReport {
modifier: 0,
modifier,
keys: [key, 0, 0, 0, 0, 0],
}
}

View file

@ -1,5 +1,6 @@
//! Host USB
use super::*;
use nusb::MaybeFuture;
/// A handler to pass requests to interface of a rusb USB device of the host
#[derive(Clone, Debug)]
@ -22,10 +23,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
setup: SetupPacket,
req: &[u8],
) -> Result<Vec<u8>> {
debug!(
"To host device: ep={:?} setup={:?} req={:?}",
ep, setup, req
);
debug!("To host device: ep={ep:?} setup={setup:?} req={req:?}",);
let mut buffer = vec![0u8; transfer_buffer_length as usize];
let timeout = std::time::Duration::new(1, 0);
let handle = self.handle.lock().unwrap();
@ -111,7 +109,7 @@ impl UsbDeviceHandler for RusbUsbHostDeviceHandler {
setup: SetupPacket,
req: &[u8],
) -> Result<Vec<u8>> {
debug!("To host device: setup={:?} req={:?}", setup, req);
debug!("To host device: setup={setup:?} req={req:?}");
let mut buffer = vec![0u8; transfer_buffer_length as usize];
let timeout = std::time::Duration::new(1, 0);
let handle = self.handle.lock().unwrap();
@ -178,48 +176,112 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
setup: SetupPacket,
req: &[u8],
) -> Result<Vec<u8>> {
debug!(
"To host device: ep={:?} setup={:?} req={:?}",
ep, setup, req
);
let mut buffer = vec![0u8; transfer_buffer_length as usize];
debug!("To host device: ep={ep:?} setup={setup:?} req={req:?}",);
let timeout = std::time::Duration::new(1, 0);
let handle = self.handle.lock().unwrap();
let control = nusb::transfer::Control {
control_type: match (setup.request_type >> 5) & 0b11 {
0 => nusb::transfer::ControlType::Standard,
1 => nusb::transfer::ControlType::Class,
2 => nusb::transfer::ControlType::Vendor,
_ => unimplemented!(),
},
recipient: match setup.request_type & 0b11111 {
0 => nusb::transfer::Recipient::Device,
1 => nusb::transfer::Recipient::Interface,
2 => nusb::transfer::Recipient::Endpoint,
3 => nusb::transfer::Recipient::Other,
_ => unimplemented!(),
},
request: setup.request,
value: setup.value,
index: setup.index,
};
if ep.attributes == EndpointAttributes::Control as u8 {
// control
if let Direction::In = ep.direction() {
// control in
if let Ok(len) = handle.control_in_blocking(control, &mut buffer, timeout) {
return Ok(Vec::from(&buffer[..len]));
let control_in = nusb::transfer::ControlIn {
control_type: match (setup.request_type >> 5) & 0b11 {
0 => nusb::transfer::ControlType::Standard,
1 => nusb::transfer::ControlType::Class,
2 => nusb::transfer::ControlType::Vendor,
_ => unimplemented!(),
},
recipient: match setup.request_type & 0b11111 {
0 => nusb::transfer::Recipient::Device,
1 => nusb::transfer::Recipient::Interface,
2 => nusb::transfer::Recipient::Endpoint,
3 => nusb::transfer::Recipient::Other,
_ => unimplemented!(),
},
request: setup.request,
value: setup.value,
index: setup.index,
length: transfer_buffer_length as u16,
};
if let Ok(data) = handle.control_in(control_in, timeout).wait() {
return Ok(data);
}
} else {
// control out
handle.control_out_blocking(control, req, timeout).ok();
let control_out = nusb::transfer::ControlOut {
control_type: match (setup.request_type >> 5) & 0b11 {
0 => nusb::transfer::ControlType::Standard,
1 => nusb::transfer::ControlType::Class,
2 => nusb::transfer::ControlType::Vendor,
_ => unimplemented!(),
},
recipient: match setup.request_type & 0b11111 {
0 => nusb::transfer::Recipient::Device,
1 => nusb::transfer::Recipient::Interface,
2 => nusb::transfer::Recipient::Endpoint,
3 => nusb::transfer::Recipient::Other,
_ => unimplemented!(),
},
request: setup.request,
value: setup.value,
index: setup.index,
data: req,
};
handle.control_out(control_out, timeout).wait().ok();
}
} else if ep.attributes == EndpointAttributes::Interrupt as u8 {
// interrupt
todo!("Missing blocking api for interrupt transfer in nusb")
if let Direction::In = ep.direction() {
// interrupt in
let mut endpoint = handle
.endpoint::<nusb::transfer::Interrupt, nusb::transfer::In>(ep.address)
.map_err(|e| {
std::io::Error::other(format!("Failed to open interrupt endpoint: {}", e))
})?;
let buffer = endpoint.allocate(transfer_buffer_length as usize);
let completion = endpoint.transfer_blocking(buffer, timeout);
if completion.status.is_ok() {
return Ok(completion.buffer.to_vec());
}
} else {
// interrupt out
let mut endpoint = handle
.endpoint::<nusb::transfer::Interrupt, nusb::transfer::Out>(ep.address)
.map_err(|e| {
std::io::Error::other(format!("Failed to open interrupt endpoint: {}", e))
})?;
if !req.is_empty() {
let mut buffer = endpoint.allocate(req.len());
buffer.copy_from_slice(req);
endpoint.transfer_blocking(buffer, timeout);
}
}
} else if ep.attributes == EndpointAttributes::Bulk as u8 {
// bulk
todo!("Missing blocking api for bulk transfer in nusb")
if let Direction::In = ep.direction() {
// bulk in
let mut endpoint = handle
.endpoint::<nusb::transfer::Bulk, nusb::transfer::In>(ep.address)
.map_err(|e| {
std::io::Error::other(format!("Failed to open bulk endpoint: {}", e))
})?;
let buffer = endpoint.allocate(transfer_buffer_length as usize);
let completion = endpoint.transfer_blocking(buffer, timeout);
if completion.status.is_ok() {
return Ok(completion.buffer.to_vec());
}
} else {
// bulk out
let mut endpoint = handle
.endpoint::<nusb::transfer::Bulk, nusb::transfer::Out>(ep.address)
.map_err(|e| {
std::io::Error::other(format!("Failed to open bulk endpoint: {}", e))
})?;
if !req.is_empty() {
let mut buffer = endpoint.allocate(req.len());
buffer.copy_from_slice(req);
endpoint.transfer_blocking(buffer, timeout);
}
}
}
Ok(vec![])
}
@ -260,39 +322,62 @@ impl UsbDeviceHandler for NusbUsbHostDeviceHandler {
setup: SetupPacket,
req: &[u8],
) -> Result<Vec<u8>> {
debug!("To host device: setup={:?} req={:?}", setup, req);
let mut buffer = vec![0u8; transfer_buffer_length as usize];
debug!("To host device: setup={setup:?} req={req:?}");
let timeout = std::time::Duration::new(1, 0);
let handle = self.handle.lock().unwrap();
let control = nusb::transfer::Control {
control_type: match (setup.request_type >> 5) & 0b11 {
0 => nusb::transfer::ControlType::Standard,
1 => nusb::transfer::ControlType::Class,
2 => nusb::transfer::ControlType::Vendor,
_ => unimplemented!(),
},
recipient: match setup.request_type & 0b11111 {
0 => nusb::transfer::Recipient::Device,
1 => nusb::transfer::Recipient::Interface,
2 => nusb::transfer::Recipient::Endpoint,
3 => nusb::transfer::Recipient::Other,
_ => unimplemented!(),
},
request: setup.request,
value: setup.value,
index: setup.index,
};
// control
if cfg!(not(target_os = "windows")) {
if setup.request_type & 0x80 == 0 {
// control out
#[cfg(not(target_os = "windows"))]
handle.control_out_blocking(control, req, timeout).ok();
{
let control_out = nusb::transfer::ControlOut {
control_type: match (setup.request_type >> 5) & 0b11 {
0 => nusb::transfer::ControlType::Standard,
1 => nusb::transfer::ControlType::Class,
2 => nusb::transfer::ControlType::Vendor,
_ => unimplemented!(),
},
recipient: match setup.request_type & 0b11111 {
0 => nusb::transfer::Recipient::Device,
1 => nusb::transfer::Recipient::Interface,
2 => nusb::transfer::Recipient::Endpoint,
3 => nusb::transfer::Recipient::Other,
_ => unimplemented!(),
},
request: setup.request,
value: setup.value,
index: setup.index,
data: req,
};
handle.control_out(control_out, timeout).wait().ok();
}
} else {
// control in
#[cfg(not(target_os = "windows"))]
if let Ok(len) = handle.control_in_blocking(control, &mut buffer, timeout) {
return Ok(Vec::from(&buffer[..len]));
{
let control_in = nusb::transfer::ControlIn {
control_type: match (setup.request_type >> 5) & 0b11 {
0 => nusb::transfer::ControlType::Standard,
1 => nusb::transfer::ControlType::Class,
2 => nusb::transfer::ControlType::Vendor,
_ => unimplemented!(),
},
recipient: match setup.request_type & 0b11111 {
0 => nusb::transfer::Recipient::Device,
1 => nusb::transfer::Recipient::Interface,
2 => nusb::transfer::Recipient::Endpoint,
3 => nusb::transfer::Recipient::Other,
_ => unimplemented!(),
},
request: setup.request,
value: setup.value,
index: setup.index,
length: transfer_buffer_length as u16,
};
if let Ok(data) = handle.control_in(control_in, timeout).wait() {
return Ok(data);
}
}
}
} else {

View file

@ -3,6 +3,7 @@
use log::*;
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use nusb::MaybeFuture;
use rusb::*;
use std::any::Any;
use std::collections::{HashMap, VecDeque};
@ -58,13 +59,10 @@ impl UsbIpServer {
pub fn with_nusb_devices(nusb_device_infos: Vec<nusb::DeviceInfo>) -> Vec<UsbDevice> {
let mut devices = vec![];
for device_info in nusb_device_infos {
let dev = match device_info.open() {
let dev = match device_info.open().wait() {
Ok(dev) => dev,
Err(err) => {
warn!(
"Impossible to open device {:?}: {}, ignoring device",
device_info, err
);
warn!("Impossible to open device {device_info:?}: {err}, ignoring device",);
continue;
}
};
@ -72,8 +70,7 @@ impl UsbIpServer {
Ok(cfg) => cfg,
Err(err) => {
warn!(
"Impossible to get active configuration {:?}: {}, ignoring device",
device_info, err
"Impossible to get active configuration {device_info:?}: {err}, ignoring device",
);
continue;
}
@ -82,7 +79,7 @@ impl UsbIpServer {
for intf in cfg.interfaces() {
// ignore alternate settings
let intf_num = intf.interface_number();
let intf = dev.claim_interface(intf_num).unwrap();
let intf = dev.claim_interface(intf_num).wait().unwrap();
let alt_setting = intf.descriptors().next().unwrap();
let mut endpoints = vec![];
@ -104,25 +101,29 @@ impl UsbIpServer {
interface_subclass: alt_setting.subclass(),
interface_protocol: alt_setting.protocol(),
endpoints,
string_interface: alt_setting.string_index().unwrap_or(0),
string_interface: alt_setting.string_index().map(|nz| nz.get()).unwrap_or(0),
class_specific_descriptor: Vec::new(),
handler,
});
}
// Platform-specific bus number (Linux-only)
let bus_num_val: u32;
#[cfg(target_os = "linux")]
{
bus_num_val = device_info.busnum() as u32;
}
#[cfg(not(target_os = "linux"))]
{
bus_num_val = 0;
}
let device_address = device_info.device_address();
let mut device = UsbDevice {
path: format!(
"/sys/bus/{}/{}/{}",
device_info.bus_number(),
device_info.device_address(),
0
),
bus_id: format!(
"{}-{}-{}",
device_info.bus_number(),
device_info.device_address(),
0,
),
bus_num: device_info.bus_number() as u32,
path: format!("/sys/bus/{}/{}/{}", bus_num_val, device_address, 0),
bus_id: format!("{}-{}-{}", bus_num_val, device_address, 0),
bus_num: bus_num_val,
dev_num: 0,
speed: device_info.speed().unwrap() as u32,
vendor_id: device_info.vendor_id(),
@ -178,8 +179,7 @@ impl UsbIpServer {
Ok(desc) => desc,
Err(err) => {
warn!(
"Impossible to get device descriptor for {:?}: {}, ignoring device",
dev, err
"Impossible to get device descriptor for {dev:?}: {err}, ignoring device",
);
continue;
}
@ -188,8 +188,7 @@ impl UsbIpServer {
Ok(desc) => desc,
Err(err) => {
warn!(
"Impossible to get config descriptor for {:?}: {}, ignoring device",
dev, err
"Impossible to get config descriptor for {dev:?}: {err}, ignoring device",
);
continue;
}
@ -319,7 +318,7 @@ impl UsbIpServer {
let open_device = match dev.open() {
Ok(dev) => dev,
Err(err) => {
warn!("Impossible to share {:?}: {}, ignoring device", dev, err);
warn!("Impossible to share {dev:?}: {err}, ignoring device");
continue;
}
};
@ -377,7 +376,7 @@ impl UsbIpServer {
} else {
Err(std::io::Error::new(
ErrorKind::NotFound,
format!("Device {} not found", bus_id),
format!("Device {bus_id} not found"),
))
}
}
@ -471,13 +470,13 @@ pub async fn handler<T: AsyncReadExt + AsyncWriteExt + Unpin>(
let res = match device.find_ep(real_ep as u8) {
None => {
warn!("Endpoint {:02x?} not found", real_ep);
warn!("Endpoint {real_ep:02x?} not found");
UsbIpResponse::usbip_ret_submit_fail(&header)
}
Some((ep, intf)) => {
trace!("->Endpoint {:02x?}", ep);
trace!("->Setup {:02x?}", setup);
trace!("->Request {:02x?}", data);
trace!("->Endpoint {ep:02x?}");
trace!("->Setup {setup:02x?}");
trace!("->Request {data:02x?}");
let resp = device
.handle_urb(
ep,
@ -493,12 +492,12 @@ pub async fn handler<T: AsyncReadExt + AsyncWriteExt + Unpin>(
if out {
trace!("<-Wrote {}", data.len());
} else {
trace!("<-Resp {:02x?}", resp);
trace!("<-Resp {resp:02x?}");
}
UsbIpResponse::usbip_ret_submit_success(&header, 0, 0, resp, vec![])
}
Err(err) => {
warn!("Error handling URB: {}", err);
warn!("Error handling URB: {err}");
UsbIpResponse::usbip_ret_submit_fail(&header)
}
}
@ -511,7 +510,7 @@ pub async fn handler<T: AsyncReadExt + AsyncWriteExt + Unpin>(
mut header,
unlink_seqnum,
} => {
trace!("Got USBIP_CMD_UNLINK for {:10x?}", unlink_seqnum);
trace!("Got USBIP_CMD_UNLINK for {unlink_seqnum:10x?}");
header.command = USBIP_RET_UNLINK.into();
@ -535,11 +534,11 @@ pub async fn server(addr: SocketAddr, server: Arc<UsbIpServer>) {
let new_server = server.clone();
tokio::spawn(async move {
let res = handler(&mut socket, new_server).await;
info!("Handler ended with {:?}", res);
info!("Handler ended with {res:?}");
});
}
Err(err) => {
warn!("Got error {:?}", err);
warn!("Got error {err:?}");
}
}
}

View file

@ -149,8 +149,7 @@ impl UsbIpCommand {
if version != 0 && version != USBIP_VERSION {
return Err(std::io::Error::other(format!(
"Unknown version: {:#04X}",
version
"Unknown version: {version:#04X}"
)));
}
@ -243,8 +242,7 @@ impl UsbIpCommand {
})
}
_ => Err(std::io::Error::other(format!(
"Unknown command: {:#04X}",
command
"Unknown command: {command:#04X}"
))),
}
}
@ -386,8 +384,8 @@ impl UsbIpResponse {
let mut result =
Vec::with_capacity(48 + transfer_buffer.len() + iso_packet_descriptor.len());
debug_assert!(header.command == USBIP_RET_SUBMIT.into());
debug_assert!(if header.direction == Direction::In as u32 {
assert!(header.command == USBIP_RET_SUBMIT.into());
assert!(if header.direction == Direction::In as u32 {
actual_length == transfer_buffer.len() as u32
} else {
actual_length == 0
@ -407,7 +405,7 @@ impl UsbIpResponse {
Self::UsbIpRetUnlink { ref header, status } => {
let mut result = Vec::with_capacity(48);
debug_assert!(header.command == USBIP_RET_UNLINK.into());
assert!(header.command == USBIP_RET_UNLINK.into());
result.extend_from_slice(&header.to_bytes());
result.extend_from_slice(&status.to_be_bytes());
@ -623,7 +621,7 @@ mod tests {
fn byte_serialize_op_rep_devlist() {
setup_test_logger();
let device = example_device();
let res = UsbIpResponse::op_rep_devlist(&[device.clone()]);
let res = UsbIpResponse::op_rep_devlist(std::slice::from_ref(&device));
assert_eq!(
res.to_bytes(),
[