This commit is contained in:
Yureka 2024-07-14 20:00:23 +02:00
commit b09fe33dd8
20 changed files with 4460 additions and 0 deletions

739
Cargo.lock generated Normal file
View file

@ -0,0 +1,739 @@
# 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 = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "assert_matches"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
[[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 = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bytes"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]]
name = "cc"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
[[package]]
name = "colorchoice"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "env_filter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
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 = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[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"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "iusb-spoof"
version = "0.1.0"
dependencies = [
"assert_matches",
"bytes",
"clap",
"env_logger",
"futures-lite",
"log",
"num_enum",
"pretty-hex",
"tempfile",
"thiserror",
"tokio",
"tokio-util",
]
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[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 = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "num_enum"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "object"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
dependencies = [
"memchr",
]
[[package]]
name = "parking"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pretty-hex"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254"
[[package]]
name = "proc-macro-crate"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
dependencies = [
"toml_datetime",
"toml_edit",
]
[[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 = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[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.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[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 = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
name = "thiserror"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-util"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
[[package]]
name = "toml_edit"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[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-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.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.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.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.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.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.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.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 = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]

29
Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "iusb-spoof"
version = "0.1.0"
authors = ["Gaelan Steele <gbs@canishe.com>", "Erik Schilling <erik.schilling@linaro.org>", "Yureka <yuka@yuka.dev>"]
description = "vhost scsi backend device"
repository = "https://git.lix.systems/the-distro/iusb-spoof"
readme = "README.md"
keywords = ["scsi", "s2600kp", "iusb", "jviewer"]
license = "Apache-2.0 OR BSD-3-Clause"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures-lite = "2.3"
bytes = "1.6.1"
clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11"
log = "0.4"
num_enum = "0.7"
pretty-hex = "0.4.1"
thiserror = "1.0"
tokio = { version = "1.38.0", features = ["rt", "net", "io-util"] }
tokio-util = { version = "0.7.11", features = ["codec"] }
[dev-dependencies]
assert_matches = "1.5"
tempfile = "3.10.1"

202
LICENSE-APACHE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

26
LICENSE-BSD-3-Clause Normal file
View file

@ -0,0 +1,26 @@
Copyright 2022 The rust-vmm authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# iusb-spoof
Based off [iusb_spoof.py](https://github.com/samozy/iusb/blob/master/iusb_spoof.py)
ISCSI Emulated Target code from [vhost-device-scsi](https://github.com/rust-vmm/vhost-device/tree/main/vhost-device-scsi)

34
make-token.py Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env python3
import re
import requests
import sys
hostname = sys.argv[1]
s = requests.Session()
r = s.post('http://'+hostname+':80/rpc/WEBSES/create.asp',data="WEBVAR_USERNAME=root&WEBVAR_PASSWORD=root")
session_cookie_regex = re.compile(r"'?(\w*(?:session)|(?:SESSION)\w*)'?\s*[:=]\s*'(\w+)'")
for line in r.text.split("\n"):
match_obj = session_cookie_regex.search(line)
if match_obj is not None:
session_cookie_value = match_obj.group(2)
s.cookies.set("SessionCookie", session_cookie_value)
break
if r.status_code != 200 or not s.cookies:
raise "Login was not successful."
r = s.get('http://'+hostname+':80/Java/jviewer.jnlp')
token_regex = re.compile("<argument>(\\w{16})</argument>")
token = None
for line in r.text.split("\n"):
match_obj = token_regex.search(line)
if match_obj is not None:
token = match_obj.group(1)
break
if r.status_code != 200 or token is None:
raise "Obtaining token was not successful."
print(token)

236
src/main.rs Normal file
View file

@ -0,0 +1,236 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
mod scsi;
use std::{
fs::File,
path::PathBuf,
process::exit,
};
use futures_lite::StreamExt;
use tokio::io::AsyncWriteExt;
use tokio_util::codec::LengthDelimitedCodec;
use bytes::BufMut;
use clap::Parser;
use log::{error, warn};
use thiserror::Error as ThisError;
use crate::scsi::emulation::{
block_device::{BlockDevice, FileBackend, MediumRotationRate},
target::EmulatedTarget,
};
use crate::scsi::{Target, Request, TaskAttr};
#[derive(Debug, ThisError)]
enum Error {
#[error("More than 256 LUNs aren't currently supported")]
TooManyLUNs,
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Parser)]
struct ScsiArgs {
/// Make the images read-only.
///
/// Currently, we don't actually support writes, but sometimes we want to
/// pretend the disk is writable to work around issues with some tools that
/// use the Linux SCSI generic API.
#[arg(long = "read-only", short = 'r')]
read_only: bool,
/// Tell the guest this disk is non-rotational.
///
/// Affects some heuristics in Linux around, for example, scheduling.
#[arg(long = "solid-state")]
solid_state: bool,
host: String,
//login_port: u16,
iusb_port: u16,
auth_token: String,
/// Images against which the SCSI actions are emulated.
images: Vec<PathBuf>,
}
async fn run() -> Result<()> {
env_logger::init();
let args = ScsiArgs::parse();
let mut target = EmulatedTarget::new();
if args.images.len() > 256 {
// This is fairly simple to add; it's just a matter of supporting the right LUN
// encoding formats.
error!("Currently only up to 256 targets are supported");
return Err(Error::TooManyLUNs);
}
if !args.read_only {
warn!("Currently, only read-only images are supported. Unless you know what you're doing, you want to pass -r");
}
for image in &args.images {
println!("{:?}", image);
let mut dev = BlockDevice::new(FileBackend::new(
File::options()
.read(true)
.write(true)
.open(image)
.expect("Opening image"),
));
dev.set_write_protected(args.read_only);
dev.set_solid_state(if args.solid_state {
MediumRotationRate::NonRotating
} else {
MediumRotationRate::Unreported
});
target.add_lun(Box::new(dev));
}
let sock = tokio::net::TcpStream::connect(&(args.host.as_str(), args.iusb_port)).await?;
sock.set_nodelay(true)?;
let (read, mut write) = tokio::io::split(sock);
assert_eq!(args.auth_token.len(), 16);
let mut data = vec![];
data.put_bytes(0x00, 9);
data.put_u8(0xf2);
data.put_bytes(0x00, 21);
data.put(args.auth_token.as_bytes());
data.put_bytes(0x00, 81);
let initial_packet = make_iusb_packet(0x05, 0x01, 0x0000, 0x00, &data);
//println!("SENT:\n{}", pretty_hex(&initial_packet));
write.write_all(&initial_packet).await?;
write.flush().await?;
let mut stream = LengthDelimitedCodec::builder()
.length_field_offset(12)
.length_field_type::<u16>()
.length_adjustment(0x20)
.little_endian()
.num_skip(0) // Do not strip frame header
.new_read(read);
stream.next().await.transpose()?;
while let Some(mut packet) = stream.next().await.transpose()? {
//println!("{}", pretty_hex(&packet));
let seq_num = packet[24];
let packet = &mut packet[32..];
let id = packet[4];
let _sniffed_command = packet[9];
let sniffed_len = u16::from_le_bytes(packet[17..19].try_into().unwrap());
if sniffed_len > 0x100 {
let resp = vec![
packet[0], 0x00, 0x00, 0x00, id, 0x00, 0x00, 0x00,
0x01, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x30,
0x00, 0x00, 0x00, 0x00, 0x00
];
let packet = make_iusb_packet(0x80, 0x01, 0x0001, seq_num, resp.as_slice());
//println!("SENT:\n{}", pretty_hex(&packet));
write.write_all(&packet).await?;
write.flush().await?;
continue;
}
//println!("sniffed command: {}", sniffed_command);
//println!("sniffed len: {}", sniffed_len);
let req = Request {
id: id.into(),
cdb: &packet[9..],
prio: 0,
crn: 0,
task_attr: TaskAttr::Simple,
};
let mut cursor = std::io::Cursor::new(&[]);
let mut resp_data = vec![];
let cmd_output = target.execute_command(0, &mut cursor, &mut resp_data, req).unwrap();
//println!("{:?} {:?}", cmd_output, resp_data.len());
if cmd_output.status != 0 {
//[2024-07-14T10:45:16Z ERROR vhost_device_scsi::scsi::emulation::target] Rejecting CDB for unknown command: [241, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
//CmdOutput { status: 2, status_qualifier: 0, sense: [112, 0, 5, 0, 0, 0, 0, 10, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0] } 0
let resp = vec![
packet[0], 0x00, 0x00, 0x00, id, 0x00, 0x00, 0x00,
0x01, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, cmd_output.sense[2], cmd_output.sense[12],
0x00, 0x00, 0x00, 0x00, 0x00
];
let packet = make_iusb_packet(0x80, 0x01, 0x0001, seq_num, resp.as_slice());
//println!("SENT:\n{}", pretty_hex(&packet));
write.write_all(&packet).await?;
write.flush().await?;
continue;
}
let mut resp = vec![];
resp.put(&packet[..25]);
//resp.put_u8(packet[8]);
//resp.put_u8(packet[9]);
//resp.put_bytes(0x00, 2);
resp.put_u32_le(resp_data.len().try_into().unwrap());
resp.put(&resp_data[..]);
//println!("responding\n{}", pretty_hex(&resp_data));
let packet = make_iusb_packet(0x80, 0x01, 0x0001, seq_num, resp.as_slice());
//println!("SENT:\n{}", pretty_hex(&packet));
write.write_all(&packet).await?;
write.flush().await?;
}
Ok(())
}
fn make_iusb_packet(
device_number: u8,
interface_number: u8,
client_data: u32,
seq_num: u8,
data: &[u8],
) -> Vec<u8> {
let data_len: u32 = data.len().try_into().unwrap();
let mut packet = vec![];
packet.put(&b"IUSB "[..]);
packet.put_u8(0x01); // major ver
packet.put_u8(0x00); // minor ver
packet.put_u8(0x20); // headers length
packet.put_u8(0x00); // checksum (unused)
packet.put_u32_le(data_len); // data length
packet.put_bytes(0x0, 1); // unknown
packet.put_u8(device_number);
packet.put_u8(interface_number);
packet.put_u8(0x80); // direction (TX)
packet.put_u32_le(client_data);
packet.put_u8(seq_num);
packet.put_bytes(0x00, 7); // reserved
packet.put(data);
packet
}
fn main() {
let rt = tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap();
if let Err(e) = rt.block_on(run()) {
error!("{e}");
exit(1);
}
}

View file

@ -0,0 +1,778 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use std::{
convert::{TryFrom, TryInto},
fs::File,
io::{self, Read, Write},
num::{NonZeroU32, NonZeroU64, TryFromIntError},
ops::{Add, Div, Mul, Sub},
os::unix::prelude::*,
};
use log::{debug, error, warn};
use super::{
command::{
parse_opcode, CommandType, LunSpecificCommand, ModePageSelection, ModeSensePageControl,
ParseOpcodeResult, ReportSupportedOpCodesMode, SenseFormat, VpdPage, OPCODES,
},
mode_page::ModePage,
response_data::{respond_standard_inquiry_data, SilentlyTruncate},
target::{LogicalUnit, LunRequest},
};
use crate::scsi::{sense, CmdError, CmdOutput, TaskAttr};
pub(crate) enum MediumRotationRate {
Unreported,
NonRotating,
}
#[derive(Clone, Copy, PartialEq, PartialOrd)]
pub(crate) struct ByteOffset(u64);
impl From<u64> for ByteOffset {
fn from(value: u64) -> Self {
ByteOffset(value)
}
}
impl From<ByteOffset> for u64 {
fn from(value: ByteOffset) -> Self {
value.0
}
}
impl Div<BlockSize> for ByteOffset {
type Output = BlockOffset;
fn div(self, rhs: BlockSize) -> Self::Output {
BlockOffset(self.0 / NonZeroU64::from(rhs.0))
}
}
#[derive(Clone, Copy, PartialEq, PartialOrd)]
pub(crate) struct BlockSize(NonZeroU32);
impl From<BlockSize> for u32 {
fn from(value: BlockSize) -> Self {
u32::from(value.0)
}
}
impl TryFrom<u32> for BlockSize {
type Error = TryFromIntError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
Ok(BlockSize(NonZeroU32::try_from(value)?))
}
}
#[derive(Clone, Copy, PartialEq, PartialOrd)]
pub(crate) struct BlockOffset(u64);
impl From<BlockOffset> for u64 {
fn from(value: BlockOffset) -> Self {
value.0
}
}
impl From<u64> for BlockOffset {
fn from(value: u64) -> Self {
BlockOffset(value)
}
}
impl Add<BlockOffset> for BlockOffset {
type Output = BlockOffset;
fn add(self, rhs: BlockOffset) -> Self::Output {
BlockOffset(self.0 + rhs.0)
}
}
impl Sub<BlockOffset> for BlockOffset {
type Output = Self;
fn sub(self, rhs: BlockOffset) -> Self::Output {
BlockOffset(self.0 - rhs.0)
}
}
impl Mul<BlockSize> for BlockOffset {
type Output = ByteOffset;
fn mul(self, rhs: BlockSize) -> Self::Output {
ByteOffset(self.0 * u64::from(NonZeroU64::from(rhs.0)))
}
}
pub(crate) trait BlockDeviceBackend: Send + Sync {
fn read_exact_at(&mut self, buf: &mut [u8], offset: ByteOffset) -> io::Result<()>;
fn write_exact_at(&mut self, buf: &[u8], offset: ByteOffset) -> io::Result<()>;
fn size_in_blocks(&mut self) -> io::Result<BlockOffset>;
fn block_size(&self) -> BlockSize;
fn sync(&mut self) -> io::Result<()>;
}
pub(crate) struct FileBackend {
file: File,
block_size: BlockSize,
}
impl FileBackend {
pub fn new(file: File) -> Self {
Self {
file,
block_size: BlockSize::try_from(512).expect("512 is valid BlockSize"),
}
}
}
impl BlockDeviceBackend for FileBackend {
fn read_exact_at(&mut self, buf: &mut [u8], offset: ByteOffset) -> io::Result<()> {
self.file.read_exact_at(buf, u64::from(offset))
}
fn write_exact_at(&mut self, buf: &[u8], offset: ByteOffset) -> io::Result<()> {
self.file.write_all_at(buf, u64::from(offset))
}
fn size_in_blocks(&mut self) -> io::Result<BlockOffset> {
let len = ByteOffset::from(self.file.metadata()?.len());
assert!(u64::from(len) % NonZeroU64::from(self.block_size.0) == 0);
Ok(len / self.block_size)
}
fn block_size(&self) -> BlockSize {
self.block_size
}
fn sync(&mut self) -> io::Result<()> {
self.file.sync_data()
}
}
pub(crate) struct BlockDevice<T: BlockDeviceBackend> {
backend: T,
write_protected: bool,
rotation_rate: MediumRotationRate,
}
impl<T: BlockDeviceBackend> BlockDevice<T> {
pub(crate) const fn new(backend: T) -> Self {
Self {
backend,
write_protected: false,
rotation_rate: MediumRotationRate::Unreported,
}
}
fn read_blocks(&mut self, lba: BlockOffset, blocks: BlockOffset) -> io::Result<Vec<u8>> {
// TODO: Ideally, this would be a read_vectored directly into guest
// address space. Instead, we have an allocation and several copies.
let mut ret = vec![
0;
usize::try_from(u64::from(blocks * self.backend.block_size()))
.expect("block length in bytes should fit usize")
];
self.backend
.read_exact_at(&mut ret[..], lba * self.backend.block_size())?;
Ok(ret)
}
fn write_blocks(
&mut self,
lba: BlockOffset,
blocks: BlockOffset,
reader: &mut dyn Read,
) -> io::Result<()> {
// TODO: Avoid the copies here.
let mut buf = vec![
0;
usize::try_from(u64::from(blocks * self.backend.block_size()))
.expect("block length in bytes should fit usize")
];
reader.read_exact(&mut buf)?;
self.backend
.write_exact_at(&buf, lba * self.backend.block_size())?;
Ok(())
}
fn write_same_block(
&mut self,
lba_start: BlockOffset,
block_count: BlockOffset,
buf: &[u8],
) -> io::Result<()> {
let block_size = self.backend.block_size();
for lba in u64::from(lba_start)..u64::from(lba_start + block_count) {
let lba = BlockOffset(lba);
self.backend.write_exact_at(buf, lba * block_size)?;
}
Ok(())
}
pub fn set_write_protected(&mut self, wp: bool) {
self.write_protected = wp;
}
pub fn set_solid_state(&mut self, rotation_rate: MediumRotationRate) {
self.rotation_rate = rotation_rate;
}
}
impl<T: BlockDeviceBackend> LogicalUnit for BlockDevice<T> {
fn execute_command(
&mut self,
data_in: &mut SilentlyTruncate<&mut dyn Write>,
data_out: &mut dyn Read,
req: LunRequest,
command: LunSpecificCommand,
) -> Result<CmdOutput, CmdError> {
if req.crn != 0 {
// CRN is a weird bit of the protocol we wouldn't ever expect to be used over
// virtio-scsi; but it's allowed to set it non-zero
warn!("Received non-zero CRN: {}", req.crn);
}
if req.task_attr != TaskAttr::Simple {
// virtio-scsi spec allows us to treat all task attrs as SIMPLE.
warn!("Ignoring non-simple task attr of {:?}", req.task_attr);
}
if req.prio != 0 {
// My reading of SAM-6 is that priority is purely advisory, so it's fine to
// ignore it.
warn!("Ignoring non-zero priority of {}.", req.prio);
}
if req.naca {
// We don't support NACA, and say as much in our INQUIRY data, so if
// we get it that's an error.
warn!("Driver set NACA bit, which is unsupported.");
return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB));
}
debug!("Incoming command: {:?}", command);
match command {
LunSpecificCommand::TestUnitReady => Ok(CmdOutput::ok()),
LunSpecificCommand::ReadCapacity10 => {
match self.backend.size_in_blocks() {
Ok(size) => {
// READ CAPACITY (10) returns a 32-bit LBA, which may not be enough. If it
// isn't, we're supposed to return 0xffff_ffff and hope the driver gets the
// memo and uses the newer READ CAPACITY (16).
// n.b. this is the last block, ie (length-1), not length
let final_block: u32 = u64::from(size - BlockOffset(1))
.try_into()
.unwrap_or(0xffff_ffff);
let block_size: u32 = u32::from(self.backend.block_size());
data_in
.write_all(&u32::to_be_bytes(final_block))
.map_err(CmdError::DataIn)?;
data_in
.write_all(&u32::to_be_bytes(block_size))
.map_err(CmdError::DataIn)?;
Ok(CmdOutput::ok())
}
Err(e) => {
error!("Error getting image size: {}", e);
// TODO: Is this a reasonable sense code to send?
Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR))
}
}
}
LunSpecificCommand::ReadCapacity16 => {
match self.backend.size_in_blocks() {
Ok(size) => {
// n.b. this is the last block, ie (length-1), not length
let final_block = u64::from(size - BlockOffset(1));
let block_size = u32::from(self.backend.block_size());
data_in
.write_all(&u64::to_be_bytes(final_block))
.map_err(CmdError::DataIn)?;
data_in
.write_all(&u32::to_be_bytes(block_size))
.map_err(CmdError::DataIn)?;
// no protection stuff; 1-to-1 logical/physical blocks
data_in.write_all(&[0, 0]).map_err(CmdError::DataIn)?;
// top 2 bits: thin provisioning stuff; other 14 bits are lowest
// aligned LBA, which is zero
data_in
.write_all(&[0b1100_0000, 0])
.map_err(CmdError::DataIn)?;
// reserved
data_in.write_all(&[0; 16]).map_err(CmdError::DataIn)?;
Ok(CmdOutput::ok())
}
Err(e) => {
error!("Error getting image size: {}", e);
// TODO: Is this a reasonable sense code to send?
Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR))
}
}
}
LunSpecificCommand::ModeSense6 { mode_page, pc, dbd } => {
// we use this for the pages array if we only need a single element; lifetime
// rules mean it has to be declared here
let single_page_array: [ModePage; 1];
let pages = match mode_page {
ModePageSelection::Single(x) => {
single_page_array = [x];
&single_page_array
}
ModePageSelection::AllPageZeros => ModePage::ALL_ZERO,
};
let pages_len: u32 = pages.iter().map(|x| u32::from(x.page_length() + 2)).sum();
// SPC-6r05, 7.5.6: "Logical units that support more than 256 bytes of block
// descriptors and mode pages should implement ten-byte mode commands. The MODE
// DATA LENGTH field in the six-byte CDB header limits the transferred data to
// 256 bytes."
// Unclear what exactly we're supposed to do if we have more than 256 bytes of
// mode pages and get sent a MODE SENSE (6). In any case, we don't at the
// moment; if we ever get that much, this unwrap() will start
// crashing us and we can figure out what to do.
let pages_len = u8::try_from(pages_len).unwrap();
// mode parameter header
data_in
.write_all(&[
pages_len + 3, // size in bytes after this one
0, // medium type - 0 for SBC
if self.write_protected {
0b1001_0000 // WP, support DPOFUA
} else {
0b0001_0000 // support DPOFUA
},
0, // block desc length
])
.map_err(CmdError::DataIn)?;
if !dbd {
// TODO: Block descriptors are optional, so we currently
// don't provide them. Does any driver
// actually use them?
}
for page in pages {
match pc {
ModeSensePageControl::Current | ModeSensePageControl::Default => {
page.write(data_in).map_err(CmdError::DataIn)?;
}
ModeSensePageControl::Changeable => {
// SPC-6 6.14.3: "If the logical unit does not
// implement changeable parameters mode pages and
// the device server receives a MODE SENSE command
// with 01b in the PC field, then the device server
// shall terminate the command with CHECK CONDITION
// status, with the sense key set to ILLEGAL
// REQUEST, and the additional sense code set to
// INVALID FIELD IN CDB."
return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB));
}
ModeSensePageControl::Saved => {
return Ok(CmdOutput::check_condition(
sense::SAVING_PARAMETERS_NOT_SUPPORTED,
))
}
}
}
Ok(CmdOutput::ok())
}
LunSpecificCommand::Read10 {
dpo,
fua,
lba,
transfer_length,
} => {
if dpo {
// DPO is just a hint that the guest probably won't access
// this any time soon, so we can ignore it
debug!("Silently ignoring DPO flag");
}
if fua {
// Somewhat weirdly, SCSI supports FUA on reads. Here's the
// key bit: "A force unit access (FUA) bit set to one
// specifies that the device server shall read the logical
// blocks from… the medium. If the FUA bit is set to one
// and a volatile cache contains a more recent version of a
// logical block than… the medium, then, before reading the
// logical block, the device server shall write the logical
// block to… the medium."
// I guess the idea is that you can read something back, and
// be absolutely sure what you just read will persist.
// So for our purposes, we need to make sure whatever we
// return has been saved to disk. fsync()ing the whole image
// is a bit blunt, but does the trick.
if let Err(e) = self.backend.sync() {
error!("Error syncing file: {}", e);
return Ok(CmdOutput::check_condition(sense::TARGET_FAILURE));
}
}
// Ignore group number: AFAICT, it's for separating reads from different
// workloads in performance metrics, and we don't report anything like that
let size = match self.backend.size_in_blocks() {
Ok(size) => size,
Err(e) => {
error!("Error getting image size for read: {}", e);
return Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR));
}
};
let lba = BlockOffset(lba.into());
let transfer_length = BlockOffset(transfer_length.into());
if lba + transfer_length > size {
return Ok(CmdOutput::check_condition(
sense::LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE,
));
}
let read_result = self.read_blocks(lba, transfer_length);
match read_result {
Ok(bytes) => {
data_in.write_all(&bytes[..]).map_err(CmdError::DataIn)?;
Ok(CmdOutput::ok())
}
Err(e) => {
error!("Error reading image: {}", e);
Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR))
}
}
}
LunSpecificCommand::Write10 {
dpo,
fua,
lba,
transfer_length,
} => {
if dpo {
// DPO is just a hint that the guest probably won't access
// this any time soon, so we can ignore it
debug!("Silently ignoring DPO flag");
}
let size = match self.backend.size_in_blocks() {
Ok(size) => size,
Err(e) => {
error!("Error getting image size for read: {}", e);
return Ok(CmdOutput::check_condition(sense::TARGET_FAILURE));
}
};
let lba = BlockOffset(lba.into());
let transfer_length = BlockOffset(transfer_length.into());
if lba + transfer_length > size {
return Ok(CmdOutput::check_condition(
sense::LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE,
));
}
let write_result = self.write_blocks(lba, transfer_length, data_out);
if fua {
if let Err(e) = self.backend.sync() {
error!("Error syncing file: {}", e);
return Ok(CmdOutput::check_condition(sense::TARGET_FAILURE));
}
}
match write_result {
Ok(()) => Ok(CmdOutput::ok()),
Err(e) => {
error!("Error writing to block device: {}", e);
Ok(CmdOutput::check_condition(sense::TARGET_FAILURE))
}
}
}
LunSpecificCommand::WriteSame16 {
lba,
number_of_logical_blocks,
anchor,
} => {
// We do not support block provisioning
if anchor {
return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB));
}
// This command can be used to unmap/discard a region of blocks...
// TODO: Do something smarter and punch holes into the backend,
// for now we will just write A LOT of zeros in a very inefficient way.
let size = match self.backend.size_in_blocks() {
Ok(size) => size,
Err(e) => {
error!("Error getting image size for read: {}", e);
return Ok(CmdOutput::check_condition(sense::UNRECOVERED_READ_ERROR));
}
};
let lba = BlockOffset(lba);
let number_of_logical_blocks = BlockOffset(number_of_logical_blocks.into());
if lba + number_of_logical_blocks > size {
return Ok(CmdOutput::check_condition(
sense::LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE,
));
}
let mut buf = vec![
0;
usize::try_from(u32::from(self.backend.block_size()))
.expect("block_size should fit usize")
];
let read_result = data_out.read_exact(&mut buf);
if let Err(e) = read_result {
error!("Error reading from data_out: {}", e);
return Ok(CmdOutput::check_condition(sense::TARGET_FAILURE));
}
let write_result = self.write_same_block(lba, number_of_logical_blocks, &buf);
match write_result {
Ok(()) => Ok(CmdOutput::ok()),
Err(e) => {
error!("Error writing to block device: {}", e);
Ok(CmdOutput::check_condition(sense::TARGET_FAILURE))
}
}
}
LunSpecificCommand::Inquiry(page_code) => {
// top 3 bits 0: peripheral device code = exists and ready
// bottom 5 bits 0: device type = block device
data_in.write_all(&[0]).map_err(CmdError::DataIn)?;
if let Some(code) = page_code {
let mut out = vec![];
match code {
VpdPage::SupportedVpdPages => {
out.push(VpdPage::SupportedVpdPages.into());
out.push(VpdPage::BlockDeviceCharacteristics.into());
out.push(VpdPage::LogicalBlockProvisioning.into());
}
VpdPage::BlockDeviceCharacteristics => {
let rotation_rate: u16 = match self.rotation_rate {
MediumRotationRate::Unreported => 0,
MediumRotationRate::NonRotating => 1,
};
out.extend_from_slice(&rotation_rate.to_be_bytes());
// nothing worth setting in the rest
out.extend_from_slice(&[0; 58]);
}
VpdPage::LogicalBlockProvisioning => {
out.push(0); // don't support threshold sets
out.push(0b1110_0100); // support unmapping w/ UNMAP
// and WRITE SAME (10 & 16),
// don't support anchored
// LBAs or group descriptors
out.push(0b0000_0010); // thin provisioned
out.push(0); // no threshold % support
}
_ => return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB)),
}
data_in
.write_all(&[code.into()])
.map_err(CmdError::DataIn)?;
data_in
.write_all(
&u16::try_from(out.len())
.expect("VPD page < 2^16 bits")
.to_be_bytes(),
)
.map_err(CmdError::DataIn)?;
data_in.write_all(&out).map_err(CmdError::DataIn)?;
} else {
respond_standard_inquiry_data(data_in).map_err(CmdError::DataIn)?;
}
Ok(CmdOutput::ok())
}
LunSpecificCommand::ReportSupportedOperationCodes { rctd, mode } => {
// helpers for output data format
fn one_command_supported(
data_in: &mut impl Write,
ty: CommandType,
) -> io::Result<()> {
data_in.write_all(&[0])?; // unused flags
data_in.write_all(&[0b0000_0011])?; // supported, don't set a bunch of flags
let tpl = ty.cdb_template();
data_in.write_all(
&u16::try_from(tpl.len())
.expect("length of TPL to be same as CDB")
.to_be_bytes(),
)?;
data_in.write_all(tpl)?;
Ok(())
}
fn one_command_not_supported(data_in: &mut impl Write) -> io::Result<()> {
data_in.write_all(&[0])?; // unused flags
data_in.write_all(&[0b0000_0001])?; // not supported
data_in.write_all(&[0; 2])?; // cdb len
Ok(())
}
fn timeout_descriptor(data_in: &mut impl Write) -> io::Result<()> {
// timeout descriptor
data_in.write_all(&0xa_u16.to_be_bytes())?; // len
data_in.write_all(&[0, 0])?; // reserved, cmd specific
data_in.write_all(&0_u32.to_be_bytes())?;
data_in.write_all(&0_u32.to_be_bytes())?;
Ok(())
}
match mode {
ReportSupportedOpCodesMode::All => {
let cmd_len = if rctd { 20 } else { 8 };
let len = u32::try_from(OPCODES.len() * cmd_len)
.expect("less than (2^32 / 20) ~= 2^27 opcodes");
data_in
.write_all(&len.to_be_bytes())
.map_err(CmdError::DataIn)?;
for &(ty, (opcode, sa)) in OPCODES {
data_in.write_all(&[opcode]).map_err(CmdError::DataIn)?;
data_in.write_all(&[0]).map_err(CmdError::DataIn)?; // reserved
data_in
.write_all(&sa.unwrap_or(0).to_be_bytes())
.map_err(CmdError::DataIn)?;
data_in.write_all(&[0]).map_err(CmdError::DataIn)?; // reserved
let ctdp: u8 = if rctd { 0b10 } else { 0b00 };
let servactv = u8::from(sa.is_some());
data_in
.write_all(&[ctdp | servactv])
.map_err(CmdError::DataIn)?;
data_in
.write_all(
&u16::try_from(ty.cdb_template().len())
.expect("length of TPL to be same as CDB")
.to_be_bytes(),
)
.map_err(CmdError::DataIn)?;
if rctd {
timeout_descriptor(data_in).map_err(CmdError::DataIn)?;
}
}
}
ReportSupportedOpCodesMode::OneCommand(opcode) => match parse_opcode(opcode) {
ParseOpcodeResult::Command(ty) => {
one_command_supported(data_in, ty).map_err(CmdError::DataIn)?;
if rctd {
timeout_descriptor(data_in).map_err(CmdError::DataIn)?;
}
}
ParseOpcodeResult::ServiceAction(_) => {
return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB));
}
ParseOpcodeResult::Invalid => {
warn!("Reporting that we don't support command {:#2x}. It might be worth adding.", opcode);
one_command_not_supported(data_in).map_err(CmdError::DataIn)?;
}
},
ReportSupportedOpCodesMode::OneServiceAction(opcode, sa) => {
match parse_opcode(opcode) {
ParseOpcodeResult::Command(_) => {
return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB))
}
ParseOpcodeResult::ServiceAction(unparsed_sa) => {
if let Some(ty) = unparsed_sa.parse(sa) {
one_command_supported(data_in, ty).map_err(CmdError::DataIn)?;
if rctd {
timeout_descriptor(data_in).map_err(CmdError::DataIn)?;
}
} else {
warn!("Reporting that we don't support command {:#2x}/{:#2x}. It might be worth adding.", opcode, sa);
one_command_not_supported(data_in).map_err(CmdError::DataIn)?;
}
}
ParseOpcodeResult::Invalid => {
// the spec isn't super clear what we're supposed to do here, but I
// think an invalid opcode is one for which our implementation
// "does not implement service actions", so we say invalid field in
// CDB
warn!("Reporting that we don't support command {:#2x}/{:#2x}. It might be worth adding.", opcode, sa);
return Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB));
}
}
}
ReportSupportedOpCodesMode::OneCommandOrServiceAction(opcode, sa) => {
match parse_opcode(opcode) {
ParseOpcodeResult::Command(ty) => {
if sa == 0 {
one_command_supported(data_in, ty).map_err(CmdError::DataIn)?;
if rctd {
timeout_descriptor(data_in).map_err(CmdError::DataIn)?;
}
} else {
one_command_not_supported(data_in).map_err(CmdError::DataIn)?;
}
}
ParseOpcodeResult::ServiceAction(unparsed_sa) => {
if let Some(ty) = unparsed_sa.parse(sa) {
one_command_supported(data_in, ty).map_err(CmdError::DataIn)?;
if rctd {
timeout_descriptor(data_in).map_err(CmdError::DataIn)?;
}
} else {
warn!("Reporting that we don't support command {:#2x}/{:#2x}. It might be worth adding.", opcode, sa);
one_command_not_supported(data_in).map_err(CmdError::DataIn)?;
}
}
ParseOpcodeResult::Invalid => {
warn!("Reporting that we don't support command {:#2x}[/{:#2x}]. It might be worth adding.", opcode, sa);
one_command_not_supported(data_in).map_err(CmdError::DataIn)?;
}
}
}
}
Ok(CmdOutput::ok())
}
LunSpecificCommand::RequestSense(format) => {
match format {
SenseFormat::Fixed => {
data_in
.write_all(&sense::NO_ADDITIONAL_SENSE_INFORMATION.to_fixed_sense())
.map_err(CmdError::DataIn)?;
Ok(CmdOutput::ok())
}
SenseFormat::Descriptor => {
// Don't support desciptor format.
Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB))
}
}
}
LunSpecificCommand::SynchronizeCache10 => {
// While SCSI allows just syncing a range, we just sync the entire file
match self.backend.sync() {
Ok(()) => Ok(CmdOutput::ok()),
Err(e) => {
error!("Error syncing block device: {}", e);
Ok(CmdOutput::check_condition(sense::TARGET_FAILURE))
}
}
}
}
}
}

View file

@ -0,0 +1,681 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
//! Data structures and parsing code for SCSI commands. A rough overview:
//! We need to deal with opcodes in two places: in parsing commands themselves,
//! and in implementing REPORT SUPPORTED OPERATION CODES. Therefore, we parse
//! commands in two steps. First, we parse the opcode (and sometimes service
//! action) into a `CommandType` (a C-style enum containing just the commands,
//! not their parameters), then using that, we parse the rest of the CDB and
//! obtain a `Cdb`, which consists of a `Command`, an enum representing a
//! command and its parameters, along with some fields shared across many or all
//! commands.
use std::convert::{TryFrom, TryInto};
use log::warn;
use num_enum::TryFromPrimitive;
use crate::scsi::emulation::mode_page::ModePage;
/// One of the modes supported by SCSI's REPORT LUNS command.
#[derive(PartialEq, Eq, TryFromPrimitive, Debug, Copy, Clone)]
#[repr(u8)]
pub(crate) enum ReportLunsSelectReport {
NoWellKnown = 0x0,
WellKnownOnly = 0x1,
All = 0x2,
Administrative = 0x10,
TopLevel = 0x11,
SameConglomerate = 0x12,
}
/// A type of "vital product data" page returned by SCSI's INQUIRY command.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub(crate) enum VpdPage {
Ascii(u8),
Ata, // *
BlockDeviceCharacteristics, // *
BlockDeviceCharacteristicsExt,
BlockLimits, // *
BlockLimitsExt,
CfaProfile,
DeviceConstituents,
DeviceIdentification, // *
ExtendedInquiry,
FormatPresets,
LogicalBlockProvisioning, // *
ManagementNetworkAddresses,
ModePagePolicy,
PowerCondition,
PowerConsumption,
PortocolSpecificLogicalUnit,
ProtocolSpecificPort,
Referrals,
ScsiFeatureSets,
ScsiPorts,
SoftwareInterfaceIdentification,
SupportedVpdPages, // *
ThirdPartyCopy,
UnitSerialNumber, // *
ZonedBlockDeviceCharacteristics, // *
}
// starred ones are ones Linux will use if available
#[derive(PartialEq, Eq, TryFromPrimitive, Debug, Copy, Clone)]
#[repr(u8)]
pub(crate) enum ModeSensePageControl {
Current = 0b00,
Changeable = 0b01,
Default = 0b10,
Saved = 0b11,
}
impl TryFrom<u8> for VpdPage {
type Error = ();
fn try_from(val: u8) -> Result<Self, ()> {
match val {
0x00 => Ok(Self::SupportedVpdPages),
0x1..=0x7f => Ok(Self::Ascii(val)),
0x80 => Ok(Self::UnitSerialNumber),
0x83 => Ok(Self::DeviceIdentification),
0x84 => Ok(Self::SoftwareInterfaceIdentification),
0x85 => Ok(Self::ManagementNetworkAddresses),
0x86 => Ok(Self::ExtendedInquiry),
0x87 => Ok(Self::ModePagePolicy),
0x88 => Ok(Self::ScsiPorts),
0x89 => Ok(Self::Ata),
0x8a => Ok(Self::PowerCondition),
0x8b => Ok(Self::DeviceConstituents),
0x8c => Ok(Self::CfaProfile),
0x8d => Ok(Self::PowerConsumption),
0x8f => Ok(Self::ThirdPartyCopy),
0x90 => Ok(Self::PortocolSpecificLogicalUnit),
0x91 => Ok(Self::ProtocolSpecificPort),
0x92 => Ok(Self::ScsiFeatureSets),
0xb0 => Ok(Self::BlockLimits),
0xb1 => Ok(Self::BlockDeviceCharacteristics),
0xb2 => Ok(Self::LogicalBlockProvisioning),
0xb3 => Ok(Self::Referrals),
0xb5 => Ok(Self::BlockDeviceCharacteristicsExt),
0xb6 => Ok(Self::ZonedBlockDeviceCharacteristics),
0xb7 => Ok(Self::BlockLimitsExt),
0xb8 => Ok(Self::FormatPresets),
_ => Err(()),
}
}
}
impl From<VpdPage> for u8 {
fn from(pc: VpdPage) -> Self {
match pc {
VpdPage::Ascii(val) => val,
VpdPage::Ata => 0x89,
VpdPage::BlockDeviceCharacteristics => 0xb1,
VpdPage::BlockDeviceCharacteristicsExt => 0xb5,
VpdPage::BlockLimits => 0xb0,
VpdPage::BlockLimitsExt => 0xb7,
VpdPage::CfaProfile => 0x8c,
VpdPage::DeviceConstituents => 0x8b,
VpdPage::DeviceIdentification => 0x83,
VpdPage::ExtendedInquiry => 0x86,
VpdPage::FormatPresets => 0xb8,
VpdPage::LogicalBlockProvisioning => 0xb2,
VpdPage::ManagementNetworkAddresses => 0x85,
VpdPage::ModePagePolicy => 0x87,
VpdPage::PowerCondition => 0x8a,
VpdPage::PowerConsumption => 0x8d,
VpdPage::PortocolSpecificLogicalUnit => 0x90,
VpdPage::ProtocolSpecificPort => 0x91,
VpdPage::Referrals => 0xb3,
VpdPage::ScsiFeatureSets => 0x92,
VpdPage::ScsiPorts => 0x88,
VpdPage::SoftwareInterfaceIdentification => 0x84,
VpdPage::SupportedVpdPages => 0x00,
VpdPage::ThirdPartyCopy => 0x8f,
VpdPage::UnitSerialNumber => 0x80,
VpdPage::ZonedBlockDeviceCharacteristics => 0xb6,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum SenseFormat {
Fixed,
Descriptor,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum ModePageSelection {
AllPageZeros,
Single(ModePage),
}
#[derive(Debug)]
pub(crate) enum LunIndependentCommand {
ReportLuns(ReportLunsSelectReport),
}
#[derive(Debug)]
pub(crate) enum LunSpecificCommand {
Inquiry(Option<VpdPage>),
ModeSense6 {
pc: ModeSensePageControl,
mode_page: ModePageSelection,
/// Disable block descriptors
dbd: bool,
},
Read10 {
/// Disable page out (i.e. hint that this page won't be accessed again
/// soon, so we shouldn't bother caching it)
dpo: bool,
/// Force unit access (i.e. bypass cache)
fua: bool,
lba: u32,
transfer_length: u16,
},
Write10 {
/// Disable page out (i.e. hint that this page won't be accessed again
/// soon, so we shouldn't bother caching it)
dpo: bool,
/// Force unit access (i.e. bypass cache)
fua: bool,
lba: u32,
transfer_length: u16,
},
WriteSame16 {
lba: u64,
number_of_logical_blocks: u32,
anchor: bool,
},
ReadCapacity10,
ReadCapacity16,
ReportSupportedOperationCodes {
/// SCSI RCTD bit: whether we should include timeout descriptors.
rctd: bool,
mode: ReportSupportedOpCodesMode,
},
RequestSense(SenseFormat),
TestUnitReady,
SynchronizeCache10,
}
#[derive(Debug)]
pub(crate) enum Command {
LunIndependentCommand(LunIndependentCommand),
LunSpecificCommand(LunSpecificCommand),
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum CommandType {
Inquiry,
ModeSense6,
Read10,
ReadCapacity10,
ReadCapacity16,
ReportLuns,
ReportSupportedOperationCodes,
RequestSense,
TestUnitReady,
Write10,
WriteSame16,
SynchronizeCache10,
}
pub(crate) const OPCODES: &[(CommandType, (u8, Option<u16>))] = &[
(CommandType::TestUnitReady, (0x0, None)),
(CommandType::RequestSense, (0x3, None)),
(CommandType::Inquiry, (0x12, None)),
(CommandType::ModeSense6, (0x1a, None)),
(CommandType::ReadCapacity10, (0x25, None)),
(CommandType::Read10, (0x28, None)),
(CommandType::Write10, (0x2a, None)),
(CommandType::SynchronizeCache10, (0x35, None)),
(CommandType::WriteSame16, (0x93, None)),
(CommandType::ReadCapacity16, (0x9e, Some(0x10))),
(CommandType::ReportLuns, (0xa0, None)),
(
CommandType::ReportSupportedOperationCodes,
(0xa3, Some(0xc)),
),
];
#[derive(Debug, Clone, Copy)]
pub(crate) struct UnparsedServiceAction(u8);
impl UnparsedServiceAction {
pub fn parse(self, service_action: u16) -> Option<CommandType> {
OPCODES
.iter()
.find(|(_, opcode)| *opcode == (self.0, Some(service_action)))
.map(|&(ty, _)| ty)
}
}
/// See `parse_opcode`
#[derive(Debug, Clone, Copy)]
pub(crate) enum ParseOpcodeResult {
/// The opcode represents a single command.
Command(CommandType),
/// The opcode requires a service action.
ServiceAction(UnparsedServiceAction),
/// The opcode is invalid.
Invalid,
}
/// Determine the command that corresponds to a SCSI opcode.
///
/// This is a little weird. Most SCSI commands are just identified by the
/// opcode - the first byte of the CDB - but some opcodes require a second
/// byte, called the service action. Generally, each distinct service action
/// value is treated as a first-class command. But there's some weirdness
/// around parsing, especially with invalid commands: sometimes, we're
/// expected to behave differently for a valid opcode with an invalid
/// service action vs an invalid opcode.
///
/// To allow for this, we have a two-step parsing API. First, a caller
/// calls `parse_opcode` with the first byte of the CDB. This could return
/// three things:
/// - `Command`: the opcode corresponded to a single-byte command; we're done.
/// - `Invalid`: the opcode isn't recognized at all; we're done.
/// - `ServiceAction`: the opcode is the first byte of a service action; the
/// caller needs to call .parse() on the `UnparsedServiceAction` we returned
/// with the service action byte.
pub(crate) fn parse_opcode(opcode: u8) -> ParseOpcodeResult {
let found = OPCODES.iter().find(|(_, (x, _))| *x == opcode);
match found {
Some(&(ty, (_, None))) => ParseOpcodeResult::Command(ty),
Some((_, (_, Some(_)))) => {
// we found some service action that uses this opcode; so this is a
// service action opcode, and we need the service action
ParseOpcodeResult::ServiceAction(UnparsedServiceAction(opcode))
}
None => ParseOpcodeResult::Invalid,
}
}
impl CommandType {
fn from_cdb(cdb: &[u8]) -> Result<Self, ParseError> {
// TODO: Variable-length CDBs put the service action in a different
// place. This'll need to change if we ever support those. IIRC, Linux
// doesn't ever use them, so it may never be relevant.
match parse_opcode(cdb[0]) {
ParseOpcodeResult::Command(ty) => Ok(ty),
ParseOpcodeResult::ServiceAction(sa) => sa
.parse(u16::from(cdb[1] & 0b0001_1111))
.ok_or(ParseError::InvalidField),
ParseOpcodeResult::Invalid => Err(ParseError::InvalidCommand),
}
}
/// Return the SCSI "CDB usage data" (see SPC-6 6.34.3) for this command
/// type.
///
/// Basically, this consists of a structure the size of the CDB for the
/// command, starting with the opcode and service action (if any), then
/// proceeding to a bitmap of fields we recognize.
pub const fn cdb_template(self) -> &'static [u8] {
match self {
Self::TestUnitReady => &[
0x0,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0100,
],
Self::RequestSense => &[
0x3,
0b0000_0001,
0b0000_0000,
0b0000_0000,
0b1111_1111,
0b0000_0100,
],
Self::ReportLuns => &[
0xa0,
0b0000_0000,
0b1111_1111,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0000_0000,
0b0000_0100,
],
Self::ReadCapacity10 => &[
0x25,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0100,
],
Self::ReadCapacity16 => &[
0x9e,
0x10,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b0000_0000,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0000_0000,
0b0000_0100,
],
Self::ModeSense6 => &[
0x1a,
0b0000_1000,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0000_0100,
],
Self::Read10 => &[
0x28,
0b1111_1100,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0011_1111,
0b1111_1111,
0b1111_1111,
0b0000_0100,
],
Self::Write10 => &[
0x2A,
0b1111_1100,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0011_1111,
0b1111_1111,
0b1111_1111,
0b0000_0100,
],
Self::WriteSame16 => &[
0x93,
0b1111_1001,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0011_1111,
0b0000_0100,
],
Self::Inquiry => &[
0x12,
0b0000_0001,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0000_0100,
],
Self::ReportSupportedOperationCodes => &[
0xa3,
0xc,
0b1000_0111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0000_0000,
0b0000_0100,
],
Self::SynchronizeCache10 => &[
0x53,
0b0000_0010,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0011_1111,
0b1111_1111,
0b1111_1111,
0b0000_0100,
],
}
}
}
#[derive(Debug)]
pub(crate) struct Cdb {
pub command: Command,
pub allocation_length: Option<u32>,
pub naca: bool,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum ParseError {
/// The opcode (specifically the first byte of the CDB) is unknown, i.e. we
/// should respond with INVALID COMMAND OPERATION CODE
InvalidCommand,
/// Another field of the CDB (including the service action, if any) is
/// invalid, i.e. we should respond with INVALID FIELD IN CDB.
InvalidField,
/// The CDB has fewer bytes than necessary for its opcode.
TooSmall,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum ReportSupportedOpCodesMode {
All,
OneCommand(u8),
OneServiceAction(u8, u16),
OneCommandOrServiceAction(u8, u16),
}
impl Cdb {
// TODO: do we want to ensure reserved fields are 0? SCSI allows, but
// doesn't require, us to do so.
pub(crate) fn parse(cdb: &[u8]) -> Result<Self, ParseError> {
let ct = CommandType::from_cdb(cdb)?;
if cdb.len() < ct.cdb_template().len() {
return Err(ParseError::TooSmall);
}
// Shrink the cdb down to its size, so accidentally accessing fields past the
// length panics
let cdb = &cdb[..ct.cdb_template().len()];
// unwraps below are safe: they're just calling TryFrom to convert from slices
// to fixed-size arrays; in each case, we're using constant indexes and we
// verified above that they're in bounds, so none of them can panic at runtime
match ct {
CommandType::Inquiry => {
// INQUIRY
let evpd = match cdb[1] {
0 => false,
1 => true,
// obselete or reserved bits set
_ => return Err(ParseError::InvalidField),
};
let page_code_raw = cdb[2];
let page_code = match (evpd, page_code_raw) {
(false, 0) => None,
(true, pc) => Some(pc.try_into().map_err(|_| ParseError::InvalidField)?),
(false, _) => return Err(ParseError::InvalidField),
};
Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::Inquiry(page_code)),
allocation_length: Some(u32::from(u16::from_be_bytes(
cdb[3..5].try_into().unwrap(),
))),
naca: (cdb[5] & 0b0000_0100) != 0,
})
}
CommandType::ModeSense6 => {
let dbd = match cdb[1] {
0b0000_1000 => true,
0b0000_0000 => false,
_ => return Err(ParseError::InvalidField),
};
let pc = (cdb[2] & 0b1100_0000) >> 6;
let page_code = cdb[2] & 0b0011_1111;
let subpage_code = cdb[3];
let mode: ModePageSelection = match (page_code, subpage_code) {
(0x8, 0x0) => ModePageSelection::Single(ModePage::Caching),
(0x3f, 0x0) => ModePageSelection::AllPageZeros,
_ => {
warn!(
"Rejecting request for unknown mode page {:#2x}/{:#2x}.",
page_code, subpage_code
);
return Err(ParseError::InvalidField);
}
};
Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::ModeSense6 {
pc: pc.try_into().map_err(|_| ParseError::InvalidField)?,
mode_page: mode,
dbd,
}),
allocation_length: Some(u32::from(cdb[4])),
naca: (cdb[5] & 0b0000_0100) != 0,
})
}
CommandType::Read10 => {
if cdb[1] & 0b1110_0100 != 0 {
// Features (protection and rebuild assist) we don't
// support; the standard says to respond with INVALID
// FIELD IN CDB for these if unsupported
return Err(ParseError::InvalidField);
}
Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::Read10 {
dpo: cdb[1] & 0b0001_0000 != 0,
fua: cdb[1] & 0b0000_1000 != 0,
lba: u32::from_be_bytes(cdb[2..6].try_into().unwrap()),
transfer_length: u16::from_be_bytes(cdb[7..9].try_into().unwrap()),
}),
allocation_length: None,
naca: (cdb[9] & 0b0000_0100) != 0,
})
}
CommandType::Write10 => {
if cdb[1] & 0b1110_0000 != 0 {
// Feature (protection) that we don't
// support; the standard says to respond with INVALID
// FIELD IN CDB for these if unsupported
return Err(ParseError::InvalidField);
}
Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::Write10 {
dpo: cdb[1] & 0b0001_0000 != 0,
fua: cdb[1] & 0b0000_1000 != 0,
lba: u32::from_be_bytes(cdb[2..6].try_into().unwrap()),
transfer_length: u16::from_be_bytes(cdb[7..9].try_into().unwrap()),
}),
allocation_length: None,
naca: (cdb[9] & 0b0000_0100) != 0,
})
}
CommandType::WriteSame16 => {
if cdb[1] & 0b1110_0001 != 0 {
warn!("Unsupported field in WriteSame16");
// We neither support protections nor logical block provisioning
return Err(ParseError::InvalidField);
}
Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::WriteSame16 {
lba: u64::from_be_bytes(cdb[2..10].try_into().expect("lba should fit u64")),
number_of_logical_blocks: u32::from_be_bytes(
cdb[10..14].try_into().expect("block count should fit u32"),
),
anchor: (cdb[1] & 0b0001_0000) != 0,
}),
allocation_length: None,
naca: (cdb[15] & 0b0000_0100) != 0,
})
}
CommandType::SynchronizeCache10 => Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::SynchronizeCache10),
allocation_length: None,
naca: (cdb[9] & 0b0000_0100) != 0,
}),
CommandType::ReadCapacity10 => Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::ReadCapacity10),
allocation_length: None,
naca: (cdb[9] & 0b0000_0100) != 0,
}),
CommandType::ReadCapacity16 => Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::ReadCapacity16),
allocation_length: Some(u32::from_be_bytes(cdb[10..14].try_into().unwrap())),
naca: (cdb[15] & 0b0000_0100) != 0,
}),
CommandType::ReportLuns => Ok(Self {
command: Command::LunIndependentCommand(LunIndependentCommand::ReportLuns(
cdb[2].try_into().map_err(|_| ParseError::InvalidField)?,
)),
allocation_length: Some(u32::from_be_bytes(cdb[6..10].try_into().unwrap())),
naca: (cdb[9] & 0b0000_0100) != 0,
}),
CommandType::ReportSupportedOperationCodes => {
let rctd = cdb[2] & 0b1000_0000 != 0;
let mode = match cdb[2] & 0b0000_0111 {
0b000 => ReportSupportedOpCodesMode::All,
0b001 => ReportSupportedOpCodesMode::OneCommand(cdb[3]),
0b010 => ReportSupportedOpCodesMode::OneServiceAction(
cdb[3],
u16::from_be_bytes(cdb[4..6].try_into().unwrap()),
),
0b011 => ReportSupportedOpCodesMode::OneCommandOrServiceAction(
cdb[3],
u16::from_be_bytes(cdb[4..6].try_into().unwrap()),
),
_ => return Err(ParseError::InvalidField),
};
Ok(Self {
command: Command::LunSpecificCommand(
LunSpecificCommand::ReportSupportedOperationCodes { rctd, mode },
),
allocation_length: Some(u32::from_be_bytes(cdb[6..10].try_into().unwrap())),
naca: (cdb[11] & 0b0000_0100) != 0,
})
}
CommandType::RequestSense => {
let format = if cdb[1] & 0b0000_0001 == 1 {
SenseFormat::Descriptor
} else {
SenseFormat::Fixed
};
Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::RequestSense(format)),
allocation_length: Some(u32::from(cdb[4])),
naca: (cdb[5] & 0b0000_0100) != 0,
})
}
CommandType::TestUnitReady => Ok(Self {
command: Command::LunSpecificCommand(LunSpecificCommand::TestUnitReady),
allocation_length: None,
naca: (cdb[5] & 0b0000_0100) != 0,
}),
}
}
}

View file

@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use std::io::{Read, Write};
use super::{
command::{LunSpecificCommand, SenseFormat},
response_data::{respond_standard_inquiry_data, SilentlyTruncate},
target::{LogicalUnit, LunRequest},
};
use crate::scsi::{sense, CmdError, CmdError::DataIn, CmdOutput};
pub(crate) struct MissingLun;
impl LogicalUnit for MissingLun {
fn execute_command(
&mut self,
data_in: &mut SilentlyTruncate<&mut dyn Write>,
_data_out: &mut dyn Read,
_req: LunRequest,
cmd: LunSpecificCommand,
) -> Result<CmdOutput, CmdError> {
match cmd {
LunSpecificCommand::Inquiry(page_code) => {
// peripheral qualifier 0b011: logical unit not accessible
// device type 0x1f: unknown/no device type
data_in.write_all(&[0b0110_0000 | 0x1f]).map_err(DataIn)?;
match page_code {
Some(_) => {
// SPC-6 7.7.2: "If the PERIPHERAL QUALIFIER field is
// not set to 000b, the contents of the PAGE LENGTH
// field and the VPD parameters are outside the
// scope of this standard."
//
// Returning a 0 length and no data seems sensible enough.
data_in.write_all(&[0]).map_err(DataIn)?;
}
None => {
respond_standard_inquiry_data(data_in).map_err(DataIn)?;
}
}
Ok(CmdOutput::ok())
}
LunSpecificCommand::RequestSense(format) => {
match format {
SenseFormat::Fixed => {
data_in
.write_all(&sense::LOGICAL_UNIT_NOT_SUPPORTED.to_fixed_sense())
.map_err(DataIn)?;
Ok(CmdOutput::ok())
}
SenseFormat::Descriptor => {
// Don't support desciptor format.
Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB))
}
}
}
_ => Ok(CmdOutput::check_condition(
sense::LOGICAL_UNIT_NOT_SUPPORTED,
)),
}
}
}

11
src/scsi/emulation/mod.rs Normal file
View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
pub(crate) mod block_device;
mod command;
pub(crate) mod missing_lun;
pub(crate) mod mode_page;
mod response_data;
pub(crate) mod target;
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use std::io::{self, Write};
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum ModePage {
Caching,
}
impl ModePage {
pub(crate) const ALL_ZERO: &'static [Self] = &[Self::Caching];
pub(crate) const fn page_code(self) -> (u8, u8) {
match self {
Self::Caching => (0x8, 0),
}
}
pub(crate) const fn page_length(self) -> u8 {
match self {
Self::Caching => 0x12,
}
}
pub(crate) fn write(self, data_in: &mut impl Write) -> io::Result<()> {
assert_eq!(self.page_code().1, 0, "Subpages aren't supported yet.");
data_in.write_all(&[
self.page_code().0, // top 2 bits: no subpage, saving not supported
self.page_length(), // page length
])?;
match self {
Self::Caching => {
data_in.write_all(&[
// Writeback Cache Enable, lots of bits zero
// n.b. kernel logs will show WCE off; it always says
// that for read-only devices, which we are rn
0b0000_0100,
])?;
// various cache fine-tuning stuff we can't really control
data_in.write_all(&[0; 0x11])?;
}
}
Ok(())
}
}

View file

@ -0,0 +1,107 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
//! Some helpers for writing response data, shared between `BlockDevice` and
//! `MissingLun`
use std::{cmp::min, convert::TryFrom, io, io::Write};
/// A wrapper around a `Write` that silently truncates its input after a given
/// number of bytes. This matches the semantics of SCSI's ALLOCATION LENGTH
/// field; anything beyond the allocation length is silently omitted.
pub struct SilentlyTruncate<W: Write>(W, usize);
impl<W: Write> SilentlyTruncate<W> {
pub const fn new(writer: W, len: usize) -> Self {
Self(writer, len)
}
}
impl<W: Write> Write for SilentlyTruncate<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.1 == 0 {
// our goal is to silently fail, so once we've stopped actually
// writing, just pretend all writes work
return Ok(buf.len());
}
let len = min(buf.len(), self.1);
let buf = &buf[..len];
let written = self.0.write(buf)?;
self.1 -= written;
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.flush()
}
}
fn encode_lun(lun: u16) -> [u8; 8] {
let lun = u8::try_from(lun).expect("more than 255 LUNs are currently unsupported");
[0, lun, 0, 0, 0, 0, 0, 0]
}
/// Write the response data for a REPORT LUNS command.
pub fn respond_report_luns<T>(data_in: &mut impl Write, luns: T) -> io::Result<()>
where
T: IntoIterator<Item = u16>,
T::IntoIter: ExactSizeIterator,
{
let iter = luns.into_iter();
data_in.write_all(
&(u32::try_from(iter.len() * 8))
.expect("less than 256 LUNS")
.to_be_bytes(),
)?;
data_in.write_all(&[0; 4])?; // reserved
for lun in iter {
data_in.write_all(&encode_lun(lun))?;
}
Ok(())
}
/// Write the response data for a standard (i.e. not VPD) inquiry, excluding the
/// first byte (the peripheal qualifier and device type).
pub fn respond_standard_inquiry_data(data_in: &mut impl Write) -> io::Result<()> {
// TODO: Feature bits here we might want to support:
// - NormACA
// - command queueing
data_in.write_all(&[
// various bits: not removable, not part of a
// conglomerate, no info on hotpluggability
0,
0x7, // version: SPC-6
// bits: don't support NormACA, support modern LUN format
// INQUIRY data version 2
0b0001_0000 | 0x2,
91, // additional INQURIY data length
// bunch of feature bits we don't support:
0,
0,
0,
])?;
// TODO: register this or another name with T10
data_in.write_all(b"rust-vmm")?;
data_in.write_all(b"vhost-user-scsi ")?;
data_in.write_all(b"v0 ")?;
// The Linux kernel doesn't request any more than this, so any data we return
// after this point is mostly academic.
data_in.write_all(&[0; 22])?;
let product_descs: &[u16; 8] = &[
0x00c0, // SAM-6 (no version claimed)
0x05c0, // SPC-5 (no version claimed)
0x0600, // SBC-4 (no version claimed)
0x0, 0x0, 0x0, 0x0, 0x0,
];
for desc in product_descs {
data_in.write_all(&desc.to_be_bytes())?;
}
data_in.write_all(&[0; 22])?;
Ok(())
}

View file

@ -0,0 +1,143 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use std::convert::TryFrom;
use std::io::{Read, Write};
use log::error;
use super::{
command::{
Cdb, Command, LunIndependentCommand, LunSpecificCommand, ParseError, ReportLunsSelectReport,
},
missing_lun::MissingLun,
response_data::{respond_report_luns, SilentlyTruncate},
};
use crate::scsi::{sense, CmdError, CmdOutput, Request, Target, TaskAttr};
pub(crate) struct LunRequest {
pub _id: u64,
pub task_attr: TaskAttr,
pub crn: u8,
pub prio: u8,
pub _allocation_length: Option<u32>,
pub naca: bool,
}
/// A single logical unit of an emulated SCSI device.
pub(crate) trait LogicalUnit: Send + Sync {
/// Process a SCSI command sent to this logical unit.
///
/// # Return value
/// This function returns a Result, but it should return Err only in limited
/// circumstances: when something goes wrong at the transport level, such
/// as writes to `req.data_in` failing or `req.cdb` being too short.
/// Any other errors, such as invalid SCSI commands or I/O errors
/// accessing an underlying file, should result in an Ok return value
/// with a `CmdOutput` representing a SCSI-level error (i.e. CHECK
/// CONDITION status, and appropriate sense data).
fn execute_command(
&mut self,
data_in: &mut SilentlyTruncate<&mut dyn Write>,
data_out: &mut dyn Read,
parameters: LunRequest,
command: LunSpecificCommand,
) -> Result<CmdOutput, CmdError>;
}
/// A SCSI target implemented by emulating a device within vhost-device-scsi.
pub(crate) struct EmulatedTarget {
luns: Vec<Box<dyn LogicalUnit>>,
}
impl EmulatedTarget {
pub(crate) fn new() -> Self {
Self { luns: Vec::new() }
}
pub(crate) fn add_lun(&mut self, logical_unit: Box<dyn LogicalUnit>) {
self.luns.push(logical_unit);
}
pub(crate) fn luns(&self) -> impl Iterator<Item = u16> + ExactSizeIterator + '_ {
// unwrap is safe: we limit LUNs at 256
self.luns
.iter()
.enumerate()
.map(|(idx, _logical_unit)| u16::try_from(idx).unwrap())
}
}
impl Default for EmulatedTarget {
fn default() -> Self {
Self::new()
}
}
impl Target for EmulatedTarget {
fn execute_command(
&mut self,
lun: u16,
data_out: &mut dyn Read,
data_in: &mut dyn Write,
req: Request,
) -> Result<CmdOutput, CmdError> {
match Cdb::parse(req.cdb) {
Ok(cdb) => {
let mut data_in = SilentlyTruncate::new(
data_in,
cdb.allocation_length.map_or(usize::MAX, |x| x as usize),
);
match cdb.command {
Command::LunIndependentCommand(cmd) => match cmd {
LunIndependentCommand::ReportLuns(select_report) => {
match select_report {
ReportLunsSelectReport::NoWellKnown
| ReportLunsSelectReport::All => {
respond_report_luns(&mut data_in, self.luns())
.map_err(CmdError::DataIn)?;
}
ReportLunsSelectReport::WellKnownOnly
| ReportLunsSelectReport::Administrative
| ReportLunsSelectReport::TopLevel
| ReportLunsSelectReport::SameConglomerate => {
respond_report_luns(&mut data_in, vec![].into_iter())
.map_err(CmdError::DataIn)?;
}
}
Ok(CmdOutput::ok())
}
},
Command::LunSpecificCommand(cmd) => {
let req = LunRequest {
_id: req.id,
task_attr: req.task_attr,
crn: req.crn,
prio: req.prio,
_allocation_length: cdb.allocation_length,
naca: cdb.naca,
};
match self.luns.get_mut(lun as usize) {
Some(lun) => lun.execute_command(&mut data_in, data_out, req, cmd),
None => MissingLun.execute_command(&mut data_in, data_out, req, cmd),
}
}
}
}
Err(ParseError::InvalidCommand) => {
error!("Rejecting CDB for unknown command: {:?}", req.cdb);
Ok(CmdOutput::check_condition(
sense::INVALID_COMMAND_OPERATION_CODE,
))
}
// TODO: SCSI has a provision for INVALID FIELD IN CDB to include the
// index of the invalid field, but it's not clear if that's mandatory.
// In any case, QEMU omits it.
Err(ParseError::InvalidField) => {
error!("Rejecting CDB with invalid field: {:?}", req.cdb);
Ok(CmdOutput::check_condition(sense::INVALID_FIELD_IN_CDB))
}
Err(ParseError::TooSmall) => Err(CmdError::CdbTooShort),
}
}
}

View file

@ -0,0 +1,198 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use super::{do_command_fail_lun, do_command_in_lun, null_image};
use crate::scsi::{
emulation::{block_device::BlockDevice, target::EmulatedTarget},
sense,
};
#[test]
fn test_report_luns() {
let mut target = EmulatedTarget::new();
for _ in 0..5 {
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
}
let select_reports = &[0x0, 0x2]; // all but well known, all
for &sr in select_reports {
do_command_in_lun(
&mut target,
6,
&[
0xa0, // REPORT LUNS
0, // reserved
sr, // select report
0, 0, 0, // reserved
0, 0, 1, 0, // alloc length: 256
0, 0,
],
&[],
&[
0, 0, 0, 40, // length: 5*8 = 40
0, 0, 0, 0, // reserved
0, 0, 0, 0, 0, 0, 0, 0, // LUN 0
0, 1, 0, 0, 0, 0, 0, 0, // LUN 1
0, 2, 0, 0, 0, 0, 0, 0, // LUN 2
0, 3, 0, 0, 0, 0, 0, 0, // LUN 3
0, 4, 0, 0, 0, 0, 0, 0, // LUN 4
],
);
}
}
#[test]
fn test_report_luns_empty() {
let mut target = EmulatedTarget::new();
for _ in 0..5 {
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
}
// well-known only and several modes explictly defined to return an empty list
// for all but ceratin types of recieving LUNs
let select_reports = &[0x1, 0x10, 0x11, 0x12];
for &sr in select_reports {
do_command_in_lun(
&mut target,
6,
&[
0xa0, // REPORT LUNS
0, // reserved
sr, // select report
0, 0, 0, // reserved
0, 0, 1, 0, // alloc length: 256
0, 0,
],
&[],
&[
0, 0, 0, 0, // length: 0
0, 0, 0, 0, // reserved
],
);
}
}
#[test]
fn test_request_sense() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in_lun(
&mut target,
1,
&[
0x3, // REQUEST SENSE
0, // fixed format sense data
0, 0, // reserved
255, // alloc length
0, // control
],
&[],
&sense::LOGICAL_UNIT_NOT_SUPPORTED.to_fixed_sense(),
);
}
#[test]
fn test_request_sense_descriptor_format() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_fail_lun(
&mut target,
1,
&[
0x3, // REQUEST SENSE
1, // descriptor format sense data
0, 0, // reserved
255, // alloc length
0, // control
],
sense::INVALID_FIELD_IN_CDB,
);
}
#[test]
fn test_inquiry() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in_lun(
&mut target,
1,
&[
0x12, // INQUIRY
0, // EVPD bit: 0
0, // page code
1, 0, // alloc length: 256
0, // control
],
&[],
// some empty comments to get rustfmt to do something vaguely sensible
&[
0x7f, // device not accessible, unknown type
0, // features
0x7, // version
0x12, // response data format v2, HiSup = 1
91, // addl length
0, 0, 0, // unsupported features
// vendor
b'r', b'u', b's', b't', b'-', b'v', b'm', b'm', //
// product
b'v', b'h', b'o', b's', b't', b'-', b'u', b's', b'e', b'r', b'-', b's', b'c', b's',
b'i', b' ', //
// revision
b'v', b'0', b' ', b' ', //
// reserved/obselete/vendor specific
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// version descriptors
0x0, 0xc0, // SAM-6
0x05, 0xc0, // SPC-5 (no code assigned for 6 yet)
0x06, 0x0, // SBC-4
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //
// reserved
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
);
}
#[test]
fn test_other_command() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_fail_lun(
&mut target,
1,
&[
0, // TEST UNIT READY
0, 0, 0, 0, // reserved
0, // control
],
sense::LOGICAL_UNIT_NOT_SUPPORTED,
);
}
#[test]
fn test_invalid_command() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_fail_lun(
&mut target,
1,
&[
0xff, // vendor specific
0, 0, 0, 0, // reserved
0, // control
],
sense::INVALID_COMMAND_OPERATION_CODE,
);
}

View file

@ -0,0 +1,108 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
//! Tests for stuff shared between commands.
use assert_matches::assert_matches;
use std::io::ErrorKind;
use super::{do_command_fail, test_image};
use crate::scsi::{
emulation::{block_device::BlockDevice, target::EmulatedTarget},
sense, CmdError, Request, Target, TaskAttr,
};
#[test]
fn test_invalid_opcode() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
do_command_fail(
&mut target,
&[
0xff, // vendor specific, unused by us
0, 0, 0, 0, 0,
],
sense::INVALID_COMMAND_OPERATION_CODE,
);
}
#[test]
fn test_invalid_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
do_command_fail(
&mut target,
&[
0xa3, // MAINTAINANCE IN
0x1f, // vendor specific, unused by us
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
sense::INVALID_FIELD_IN_CDB,
);
}
#[test]
fn test_short_data_out_buffer() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
let mut data_in: &mut [u8] = &mut [];
let mut data_out: &[u8] = &[0_u8; 511];
let res = target.execute_command(
0,
&mut data_out,
&mut data_in,
Request {
id: 0,
cdb: &[
0x28, // READ (10)
0, // flags
0, 0, 0, 15, // LBA: 5
0, // reserved, group #
0, 1, // transfer length: 1
0, // control
],
task_attr: TaskAttr::Simple,
crn: 0,
prio: 0,
},
);
if let CmdError::DataIn(e) = res.unwrap_err() {
assert_eq!(e.kind(), ErrorKind::WriteZero);
} else {
panic!();
}
}
#[test]
fn test_short_cdb() {
let mut target: EmulatedTarget = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
let mut data_in: &mut [u8] = &mut [];
let mut data_out: &[u8] = &[];
let res = target.execute_command(
0,
&mut data_out,
&mut data_in,
Request {
id: 0,
cdb: &[
0x28, // READ (10)
],
task_attr: TaskAttr::Simple,
crn: 0,
prio: 0,
},
);
assert_matches!(res.unwrap_err(), CmdError::CdbTooShort);
}

View file

@ -0,0 +1,520 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
#![cfg(test)]
mod bad_lun;
mod generic;
mod report_supported_operation_codes;
use std::{
fs::File,
io::Write,
sync::{Arc, Mutex},
};
use tempfile::tempfile;
use super::{
block_device::{
BlockDevice, BlockDeviceBackend, BlockOffset, BlockSize, ByteOffset, FileBackend,
},
target::EmulatedTarget,
};
use crate::scsi::{
sense::{self, SenseTriple},
CmdOutput, Request, Target, TaskAttr,
};
#[derive(Clone)]
struct TestBackend {
data: Arc<Mutex<[u8; 512 * 16]>>,
}
impl TestBackend {
fn new() -> Self {
TestBackend {
data: Arc::new(Mutex::new([0; 512 * 16])),
}
}
}
impl BlockDeviceBackend for TestBackend {
fn read_exact_at(&mut self, buf: &mut [u8], offset: ByteOffset) -> std::io::Result<()> {
let data = self.data.lock().unwrap();
let offset = usize::try_from(u64::from(offset)).expect("offset should fit usize");
buf.copy_from_slice(&data[offset..(offset + buf.len())]);
Ok(())
}
fn write_exact_at(&mut self, buf: &[u8], offset: ByteOffset) -> std::io::Result<()> {
let mut data = self.data.lock().unwrap();
let offset = usize::try_from(u64::from(offset)).expect("offset should fit usize");
data[offset..(offset + buf.len())].copy_from_slice(buf);
Ok(())
}
fn size_in_blocks(&mut self) -> std::io::Result<BlockOffset> {
Ok(ByteOffset::from(
u64::try_from(self.data.lock().unwrap().len()).expect("size_in_blocks should fit u64"),
) / self.block_size())
}
fn block_size(&self) -> BlockSize {
BlockSize::try_from(512).expect("512 should be a valid BlockSize")
}
fn sync(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn null_image() -> FileBackend {
FileBackend::new(File::open("/dev/null").unwrap())
}
fn test_image() -> FileBackend {
let mut f = tempfile().unwrap();
// generate 16 512-byte sectors, each of which consist of a single
// repeated hex character, i.e.
// sector 00: 0000000....0000
// sector 15: fffffff....ffff
for chr in b'0'..=b'9' {
f.write_all(&[chr; 512]).unwrap();
}
for chr in b'a'..=b'f' {
f.write_all(&[chr; 512]).unwrap();
}
FileBackend::new(f)
}
fn do_command_in_lun(
target: &mut EmulatedTarget,
lun: u16,
cdb: &[u8],
data_out: &[u8],
expected_data_in: &[u8],
) {
let mut data_in = Vec::new();
let res = target.execute_command(
lun,
&mut &data_out[..],
&mut data_in,
Request {
id: 0,
cdb,
task_attr: TaskAttr::Simple,
crn: 0,
prio: 0,
},
);
assert_eq!(res.unwrap(), CmdOutput::ok());
assert_eq!(&data_in, expected_data_in);
}
fn do_command_fail_lun(
target: &mut EmulatedTarget,
lun: u16,
cdb: &[u8],
expected_error: SenseTriple,
) {
let mut data_in = Vec::new();
let mut data_out: &[u8] = &[];
let res = target.execute_command(
lun,
&mut data_out,
&mut data_in,
Request {
id: 0,
cdb,
task_attr: TaskAttr::Simple,
crn: 0,
prio: 0,
},
);
assert_eq!(res.unwrap(), CmdOutput::check_condition(expected_error));
assert_eq!(&data_in, &[]);
}
fn do_command_in(
target: &mut EmulatedTarget,
cdb: &[u8],
data_out: &[u8],
expected_data_in: &[u8],
) {
do_command_in_lun(target, 0, cdb, data_out, expected_data_in);
}
fn do_command_fail(target: &mut EmulatedTarget, cdb: &[u8], expected_error: SenseTriple) {
do_command_fail_lun(target, 0, cdb, expected_error);
}
fn block_size_512() -> BlockSize {
BlockSize::try_from(512).expect("512 should be a valid block_size")
}
#[test]
fn test_test_unit_ready() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(&mut target, &[0, 0, 0, 0, 0, 0], &[], &[]);
}
#[test]
fn test_report_luns() {
let mut target = EmulatedTarget::new();
for _ in 0..5 {
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
}
do_command_in(
&mut target,
&[
0xa0, // REPORT LUNS
0, // reserved
0, // select report
0, 0, 0, // reserved
0, 0, 1, 0, // alloc length: 256
0, 0,
],
&[],
&[
0, 0, 0, 40, // length: 5*8 = 40
0, 0, 0, 0, // reserved
0, 0, 0, 0, 0, 0, 0, 0, // LUN 0
0, 1, 0, 0, 0, 0, 0, 0, // LUN 1
0, 2, 0, 0, 0, 0, 0, 0, // LUN 2
0, 3, 0, 0, 0, 0, 0, 0, // LUN 3
0, 4, 0, 0, 0, 0, 0, 0, // LUN 4
],
);
}
#[test]
fn test_read_10() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
do_command_in(
&mut target,
&[
0x28, // READ (10)
0, // flags
0, 0, 0, 5, // LBA: 5
0, // reserved, group #
0, 1, // transfer length: 1
0, // control
],
&[],
&[b'5'; 512],
);
}
#[test]
fn test_read_10_last_block() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
do_command_in(
&mut target,
&[
0x28, // READ (10)
0, // flags
0, 0, 0, 15, // LBA: 5
0, // reserved, group #
0, 1, // transfer length: 1
0, // control
],
&[],
&[b'f'; 512],
);
}
#[test]
fn test_read_10_out_of_range() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
do_command_fail(
&mut target,
&[
0x28, // READ (10)
0, // flags
0, 0, 0, 16, // LBA: 16
0, // reserved, group #
0, 1, // transfer length: 1
0, // control
],
sense::LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE,
);
}
#[test]
fn test_read_10_cross_out() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
do_command_fail(
&mut target,
&[
0x28, // READ (10)
0, // flags
0, 0, 0, 15, // LBA: 15
0, // reserved, group #
0, 2, // transfer length: 2
0, // control
],
sense::LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE,
);
}
#[test]
fn test_write_10() {
let mut target = EmulatedTarget::new();
let mut backend = TestBackend::new();
let dev = BlockDevice::new(backend.clone());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
{
let data_out = [b'w'; 512];
do_command_in(
&mut target,
&[
0x2a, // WRITE (10)
0, // flags
0, 0, 0, 5, // LBA: 5
0, // reserved, group #
0, 1, // transfer length: 1
0, // control
],
&data_out,
&[],
);
let mut buf = [0_u8; 512];
backend
.read_exact_at(&mut buf, BlockOffset::from(5) * block_size_512())
.expect("Reading should work");
assert_eq!(data_out, buf);
}
}
#[test]
fn test_write_same_16() {
let mut target = EmulatedTarget::new();
let mut backend = TestBackend::new();
let dev = BlockDevice::new(backend.clone());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
backend
.write_exact_at(&[0xff; 512 * 6], BlockOffset::from(5) * block_size_512())
.expect("Write should succeed");
let data_out = [0_u8; 512];
do_command_in(
&mut target,
&[
0x93, // WRITE SAME (16)
0, // flags
0, 0, 0, 0, 0, 0, 0, 5, // LBA: 5
0, 0, 0, 5, // tnumber of blocks: 5
0, // reserved, group #
0, // control
],
&data_out,
&[],
);
let mut buf = [0_u8; 512 * 5];
backend
.read_exact_at(&mut buf, BlockOffset::from(5) * block_size_512())
.expect("Reading should work");
assert_eq!([0_u8; 512 * 5], buf, "5 sectors should have been zero'd");
let mut buf = [0_u8; 512];
backend
.read_exact_at(&mut buf, BlockOffset::from(10) * block_size_512())
.expect("Reading should work");
assert_eq!(
[0xff_u8; 512], buf,
"sector after write should be left untouched"
);
}
#[test]
fn test_read_capacity_10() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
// TODO: we should test behavior with ≥ 2 TiB images. But not sure how we
// can do that reliably without risking using 2 TiB of disk
do_command_in(
&mut target,
&[
0x25, // READ CAPACITY (10)
0, 0, 0, 0, 0, 0, 0, 0, // flags
0, // control
],
&[],
&[
0, 0, 0, 15, // returned LBA (last valid LBA),
0, 0, 2, 0, // block size (512)
],
);
}
#[test]
fn test_read_capacity_16() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(test_image());
target.add_lun(Box::new(dev));
// TODO: this test relies on the default logical block size of 512. We should
// make that explicit.
do_command_in(
&mut target,
&[
0x9e, 0x10, // READ CAPACITY (16)
0, 0, 0, 0, 0, 0, 0, 0, // obsolete
0, 0, 0, 32, // allocation length: 32
0, // obselete/reserved
0, // control
],
&[],
&[
0, 0, 0, 0, 0, 0, 0, 15, // returned LBA (last valid LBA),
0, 0, 2, 0, // block size (512)
0, // reserved, zoned stuff, protection stuff
0, // one PB per LB
0xc0, // thin provisioning, unmapped blocks read 0
0, // LBA 0 is aligned (top bits above)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // reserved
],
);
}
#[test]
fn test_inquiry() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0x12, // INQUIRY
0, // EVPD bit: 0
0, // page code
1, 0, // alloc length: 256
0, // control
],
&[],
// some empty comments to get rustfmt to do something vaguely sensible
&[
0, // accessible; direct acccess block device
0, // features
0x7, // version
0x12, // response data format v2, HiSup = 1
91, // addl length
0, 0, 0, // unsupported features
// vendor
b'r', b'u', b's', b't', b'-', b'v', b'm', b'm', //
// product
b'v', b'h', b'o', b's', b't', b'-', b'u', b's', b'e', b'r', b'-', b's', b'c', b's',
b'i', b' ', //
// revision
b'v', b'0', b' ', b' ', //
// reserved/obselete/vendor specific
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// version descriptors
0x0, 0xc0, // SAM-6
0x05, 0xc0, // SPC-5 (no code assigned for 6 yet)
0x06, 0, // SBC-4
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //
// reserved
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
);
}
#[test]
fn test_request_sense() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0x3, // INQUIRY
0, // desc bit: 0
0, 0, // reserved
255, // alloc length
0, // control
],
&[],
// We'll always return this - modern SCSI has autosense, so any errors are sent with the
// response to the command that caused them (and therefore immediately cleared), and
// REQUEST SENSE returns an actual error only under some exceptional circumstances
// we don't implement.
&sense::NO_ADDITIONAL_SENSE_INFORMATION.to_fixed_sense(),
);
}
#[test]
fn test_request_sense_descriptor_format() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_fail(
&mut target,
&[
0x3, // INQUIRY
1, // desc bit: 1
0, 0, // reserved
255, // alloc length
0, // control
],
// We don't support descriptor format sense data.
sense::INVALID_FIELD_IN_CDB,
);
}

View file

@ -0,0 +1,420 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use super::{do_command_fail, do_command_in, null_image};
use crate::scsi::{
emulation::{block_device::BlockDevice, target::EmulatedTarget},
sense,
};
#[test]
fn test_one_command() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b1, // reporting options: one command
0, 1, 2, // opcode: TEST UNIT READY, SA ignored
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 6, // cdb len
0, 0, 0, 0, 0, 0b0100, // usage data
],
);
}
#[test]
fn test_one_command_with_timeout_descriptor() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0x81, // request timeout descs, reporting options: one command
0, 1, 2, // opcode: TEST UNIT READY, SA ignored
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 6, // cdb len
0, 0, 0, 0, 0, 0b0100, // usage data
0, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // no timeouts
],
);
}
#[test]
fn test_one_command_unsupported() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b1, // reporting options: one command
0xff, 1, 2, // opcode: vendor specific, SA ignored
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b01, // flags, not supported
0, 0, // cdb len
],
);
}
#[test]
fn test_one_command_valid_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_fail(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b1, // reporting options: one command
0x9e, 0, 0x10, // SERVICE ACTION IN (16), READ CAPACITY (16)
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
sense::INVALID_FIELD_IN_CDB,
);
}
#[test]
fn test_one_command_invalid_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_fail(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b1, // reporting options: one command
0x9e, 0, 0xff, // SERVICE ACTION IN (16), invalid
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
sense::INVALID_FIELD_IN_CDB,
);
}
#[test]
fn test_one_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b10, // reporting options: one service action
0x9e, 0, 0x10, // SERVICE ACTION IN (16), READ CAPACITY (16)
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 16, // cdb len
0x9e, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0,
0b0100, // usage data
],
);
}
#[test]
fn test_one_service_action_with_timeout_descriptor() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0x82, // request timeout descs, reporting options: one service action
0x9e, 0, 0x10, // SERVICE ACTION IN (16), READ CAPACITY (16)
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 16, // cdb len
0x9e, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0,
0b0100, // usage data
0, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // no timeouts
],
);
}
#[test]
fn test_one_service_action_unknown_opcode() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
// not entirely sure this behavior is correct; see comment in implementation
do_command_fail(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b10, // reporting options: one service action
0xff, 1, 2, // opcode: vendor specific, unimplemented
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
sense::INVALID_FIELD_IN_CDB,
);
}
#[test]
fn test_one_service_action_unknown_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b10, // reporting options: one service action
0x9e, 0, 0xff, // SERVICE ACTION IN (16), invalid SA
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b01, // flags, not supported
0, 0, // cdb len
],
);
}
#[test]
fn test_one_service_action_not_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_fail(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b10, // reporting options: one service action
0, 1, 2, // TEST UNIT READY
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
sense::INVALID_FIELD_IN_CDB,
);
}
// rest of these tests are for "mode 3", which the spec calls 011b and our
// implementation calls OneCommandOrServiceAction, but that's a mouthful so just
// use "mode 3" for test names
#[test]
fn test_mode_3_opcode_without_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b11, // reporting options: mode 3
0, 0, 0, // opcode: TEST UNIT READY, SA: 0
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 6, // cdb len
0, 0, 0, 0, 0, 0b0100, // usage data
],
);
}
#[test]
fn test_mode_3_with_timeout_descriptor() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0x83, // request timeout descs, reporting options: mode 3
0, 0, 0, // opcode: TEST UNIT READY, SA: 0
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 6, // cdb len
0, 0, 0, 0, 0, 0b0100, // usage data
0, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // no timeouts
],
);
}
#[test]
fn test_mode_3_opcode_with_unnecessary_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b11, // reporting options: mode 3
0, 0, 1, // opcode: TEST UNIT READY, SA: 1
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b01, // flags, not supported
0, 0, // cdb len
],
);
}
#[test]
fn test_mode_3_invalid_opcode() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b11, // reporting options: mode 3
0xff, 0, 0, // opcode: vendor specific
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b01, // flags, not supported
0, 0, // cdb len
],
);
}
#[test]
fn test_mode_3_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b11, // reporting options: mode 3
0x9e, 0, 0x10, // opcode: SERVICE ACTION IN (16), READ CAPACITY (16)
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 16, // cdb len
0x9e, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0,
0b0100, // usage data
],
);
}
#[test]
fn test_mode_3_service_action_with_timeout_descriptor() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0x83, // request timeout desc, tireporting options: mode 3
0x9e, 0, 0x10, // opcode: SERVICE ACTION IN (16), READ CAPACITY (16)
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b11, // flags, supported
0, 16, // cdb len
0x9e, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0,
0b0100, // usage data
0, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // no timeouts
],
);
}
#[test]
fn test_mode_3_invalid_service_action() {
let mut target = EmulatedTarget::new();
let dev = BlockDevice::new(null_image());
target.add_lun(Box::new(dev));
do_command_in(
&mut target,
&[
0xa3, 0x0c, // REPORT SUPPORTED OPERATION CODES
0b11, // reporting options: mode 3
0x9e, 0, 0xff, // opcode: SERVICE ACTION IN (16), invalid SA
0, 0, 1, 0, // allocation length: 256
0, // reserved
0, // control
],
&[],
&[
0, 0b01, // flags, not supported
0, 0, // cdb len
],
);
}

76
src/scsi/mod.rs Normal file
View file

@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
pub mod emulation;
pub mod sense;
use std::io::{self, Read, Write};
use self::sense::SenseTriple;
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum TaskAttr {
Simple,
Ordered,
HeadOfQueue,
Aca,
}
#[derive(Debug, PartialEq, Eq)]
pub struct CmdOutput {
pub status: u8,
pub status_qualifier: u16,
pub sense: Vec<u8>,
}
impl CmdOutput {
pub const fn ok() -> Self {
Self {
status: 0,
status_qualifier: 0,
sense: Vec::new(),
}
}
pub fn check_condition(sense: SenseTriple) -> Self {
Self {
status: 2,
status_qualifier: 0,
sense: sense.to_fixed_sense(),
}
}
}
pub struct Request<'a> {
pub id: u64,
pub cdb: &'a [u8],
pub task_attr: TaskAttr,
pub crn: u8,
pub prio: u8,
}
/// An transport-level error encountered while processing a SCSI command.
///
/// This is only for transport-level errors; anything else should be handled by
/// returning a CHECK CONDITION status at the SCSI level.
#[derive(Debug)]
pub enum CmdError {
/// The provided CDB is too short for its operation code.
CdbTooShort,
/// An error occurred while writing to the provided data in writer.
DataIn(io::Error),
}
/// A transport-independent implementation of a SCSI target.
///
/// Currently, we only support emulated targets (see the `emulation` module),
/// but other implementations of this trait could implement pass-through to
/// iSCSI targets or SCSI devices on the host.
pub trait Target: Send + Sync {
fn execute_command(
&mut self,
lun: u16,
data_out: &mut dyn Read,
data_in: &mut dyn Write,
req: Request,
) -> Result<CmdOutput, CmdError>;
}

37
src/scsi/sense.rs Normal file
View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct SenseTriple(u8, u8, u8);
impl SenseTriple {
pub fn to_fixed_sense(self) -> Vec<u8> {
vec![
0x70, // response code (fixed, current); valid bit (0)
0x0, // reserved
self.0, // sk; various upper bits 0
0x0, 0x0, 0x0, 0x0, // information
0xa, // add'l sense length
0x0, 0x0, 0x0, 0x0, // cmd-specific information
self.1, // asc
self.2, // ascq
0x0, // field-replacable unit code
0x0, 0x0, 0x0, // sense-key-sepcific information
]
}
}
const NO_SENSE: u8 = 0;
const MEDIUM_ERROR: u8 = 0x3;
const HARDWARE_ERROR: u8 = 0x4;
const ILLEGAL_REQUEST: u8 = 0x5;
pub const NO_ADDITIONAL_SENSE_INFORMATION: SenseTriple = SenseTriple(NO_SENSE, 0, 0);
pub const INVALID_COMMAND_OPERATION_CODE: SenseTriple = SenseTriple(ILLEGAL_REQUEST, 0x20, 0x0);
pub const LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE: SenseTriple = SenseTriple(ILLEGAL_REQUEST, 0x21, 0x0);
pub const INVALID_FIELD_IN_CDB: SenseTriple = SenseTriple(ILLEGAL_REQUEST, 0x24, 0x0);
pub const LOGICAL_UNIT_NOT_SUPPORTED: SenseTriple = SenseTriple(ILLEGAL_REQUEST, 0x21, 0x0);
pub const SAVING_PARAMETERS_NOT_SUPPORTED: SenseTriple = SenseTriple(ILLEGAL_REQUEST, 0x39, 0x0);
pub const UNRECOVERED_READ_ERROR: SenseTriple = SenseTriple(MEDIUM_ERROR, 0x11, 0x0);
pub const TARGET_FAILURE: SenseTriple = SenseTriple(HARDWARE_ERROR, 0x44, 0x0);