Merge remote-tracking branch 'origin/upstream'
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..7c51c84
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,33 @@
+// This file is generated by cargo_embargo.
+// Do not modify this file because the changes will be overridden on upgrade.
+
+package {
+    default_applicable_licenses: ["external_rust_crates_rusb_license"],
+    default_team: "trendy_team_android_rust",
+}
+
+license {
+    name: "external_rust_crates_rusb_license",
+    visibility: [":__subpackages__"],
+    license_kinds: ["SPDX-license-identifier-MIT"],
+    license_text: ["LICENSE"],
+}
+
+rust_library {
+    name: "librusb",
+    host_supported: true,
+    crate_name: "rusb",
+    cargo_env_compat: true,
+    cargo_pkg_version: "0.9.4",
+    crate_root: "src/lib.rs",
+    edition: "2018",
+    rustlibs: [
+        "liblibc",
+        "liblibusb1_sys",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "//apex_available:anyapex",
+    ],
+    vendor_available: true,
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..de4a6d7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,87 @@
+# Changes
+
+## 0.9.4
+
+* bLength, bDescriptorType and wTotalLength to descriptors [#185]
+* Use &self reference for all DeviceHandle methods [#186]
+* fix: panic when trying to iterate over an interface with zero endpoints [#195]
+* Log callback API added [#194]
+* Bump libusb1-sys 0.7.0 [#205]
+
+[#185]: https://github.com/a1ien/rusb/pull/185
+[#186]: https://github.com/a1ien/rusb/pull/186
+[#195]: https://github.com/a1ien/rusb/pull/195
+[#194]: https://github.com/a1ien/rusb/pull/194
+[#205]: https://github.com/a1ien/rusb/pull/205
+
+## 0.9.3
+* impl serde::{Serialize, Deserialize} for public enums [#167]
+* Update deprecated doc link about language identifiers [#165]
+* Fix changelog URLs for 0.9.2 [#164]
+
+
+[#167]: https://github.com/a1ien/rusb/pull/167
+[#165]: https://github.com/a1ien/rusb/pull/165
+[#164]: https://github.com/a1ien/rusb/pull/164
+
+## 0.9.2
+* Random corrections around the code [#127]
+* examples: list_devices: Add vendor and product name [#128]
+* examples: read_devices: Improve usage [#125]
+* context: create rusb `Context` from existing `libusb_context` [#135]
+* `new` now uses `from_raw` [#135]
+* Fix stack use after scope in tests [#138]
+* Fix United Kingdom misspelling in languages docs [#137]
+* fields.rs: Make request_type function a const fn [#142]
+* Increase endpoint descriptor's lifetime [#149]
+* Fix timeout documentation [#151]
+
+[#127]: https://github.com/a1ien/rusb/pull/127
+[#128]: https://github.com/a1ien/rusb/pull/128
+[#125]: https://github.com/a1ien/rusb/pull/125
+[#135]: https://github.com/a1ien/rusb/pull/135
+[#138]: https://github.com/a1ien/rusb/pull/135
+[#137]: https://github.com/a1ien/rusb/pull/137
+[#142]: https://github.com/a1ien/rusb/pull/142
+[#149]: https://github.com/a1ien/rusb/pull/149
+[#151]: https://github.com/a1ien/rusb/pull/151
+
+## 0.9.1
+* impl Ord and PartialOrd for Version [#116]
+
+[#116]: https://github.com/a1ien/rusb/pull/116
+
+## 0.9.0
+* Re-export libusb1-sys as ffi [#75]
+* impl Debug for DeviceHandle [#78]
+* Add bind to libusb_get_next_timeout [#95]
+* Add DeviceHandle::into_raw() [#97]
+* Improve read_string_descriptor [#99]
+* Derive Debug for Context [#103]
+* Implement Clone for Device [#104]
+* Add Context::interrupt_handle_events() [#101]
+* context: add open_device_with_fd() [#106]
+* Rewrite hotplug registration. Add `HotplugBuilder` [#110]. And rewrite [#72]
+* ConfigDescriptor and InterfaceDescriptor extra return just slice [#111]
+
+[#72]: https://github.com/a1ien/rusb/pull/72
+[#75]: https://github.com/a1ien/rusb/pull/75
+[#78]: https://github.com/a1ien/rusb/pull/78
+[#95]: https://github.com/a1ien/rusb/pull/95
+[#97]: https://github.com/a1ien/rusb/pull/97
+[#99]: https://github.com/a1ien/rusb/pull/99
+[#101]: https://github.com/a1ien/rusb/pull/101
+[#103]: https://github.com/a1ien/rusb/pull/103
+[#104]: https://github.com/a1ien/rusb/pull/104
+[#106]: https://github.com/a1ien/rusb/pull/106
+[#110]: https://github.com/a1ien/rusb/pull/110
+[#111]: https://github.com/a1ien/rusb/pull/111
+
+## 0.8.1
+* Add getters for bRefresh and bSynchAddress [#61]
+* Implement Display for Version. [#59]
+* Add Device/DeviceHandle::context getter methods [#57]
+
+[#61]: https://github.com/a1ien/rusb/pull/61
+[#59]: https://github.com/a1ien/rusb/pull/59
+[#57]: https://github.com/a1ien/rusb/pull/57
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..7bfeaec
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,285 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "getrandom"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+
+[[package]]
+name = "libusb1-sys"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "phf"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "963adb11cf22ee65dfd401cf75577c1aa0eca58c0b97f9337d2da61d3e640503"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "regex"
+version = "1.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
+
+[[package]]
+name = "rusb"
+version = "0.9.4"
+dependencies = [
+ "libc",
+ "libusb1-sys",
+ "regex",
+ "serde",
+ "usb-ids",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "syn"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+
+[[package]]
+name = "usb-ids"
+version = "1.2023.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f6ee7f9e28c6c96a3432e89e5eaf19f88e4a4c477e9db208ab083405ee8a765"
+dependencies = [
+ "nom",
+ "phf",
+ "phf_codegen",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0ce0f6a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,54 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "rusb"
+version = "0.9.4"
+authors = [
+    "David Cuddeback <[email protected]>",
+    "Ilya Averyanov <[email protected]>",
+]
+build = "build.rs"
+description = "Rust library for accessing USB devices."
+homepage = "https://github.com/a1ien/rusb"
+readme = "README.md"
+keywords = [
+    "usb",
+    "libusb",
+    "hardware",
+    "bindings",
+]
+license = "MIT"
+repository = "https://github.com/a1ien/rusb.git"
+
+[dependencies.libc]
+version = "0.2"
+
+[dependencies.libusb1-sys]
+version = "0.7"
+
+[dependencies.serde]
+version = "1.0"
+features = ["derive"]
+optional = true
+
+[dev-dependencies.regex]
+version = "1"
+
+[dev-dependencies.usb-ids]
+version = "1.2023.0"
+
+[features]
+vendored = ["libusb1-sys/vendored"]
+
+[badges.travis-ci]
+repository = "a1ien/rusb"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..e18b645
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,30 @@
+[package]
+name = "rusb"
+version = "0.9.4"
+authors = ["David Cuddeback <[email protected]>", "Ilya Averyanov <[email protected]>"]
+description = "Rust library for accessing USB devices."
+license = "MIT"
+homepage = "https://github.com/a1ien/rusb"
+repository = "https://github.com/a1ien/rusb.git"
+readme = "README.md"
+keywords = ["usb", "libusb", "hardware", "bindings"]
+edition = "2018"
+build = "build.rs"
+
+[badges]
+travis-ci = { repository = "a1ien/rusb" }
+
+[features]
+vendored = [ "libusb1-sys/vendored" ]
+
+[workspace]
+members = ["libusb1-sys"]
+
+[dependencies]
+libusb1-sys = { path = "libusb1-sys", version = "0.7" }
+libc = "0.2"
+serde = { version = "1.0", features = ["derive"], optional = true }
+
+[dev-dependencies]
+regex = "1"
+usb-ids = "1.2023.0"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..cf8ea43
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2015 David Cuddeback
+              2019 Ilya Averyanov
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..fcbe2c9
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "rusb"
+description: "Rust library for accessing USB devices."
+third_party {
+  identifier {
+    type: "crates.io"
+    value: "rusb"
+  }
+  identifier {
+    type: "Archive"
+    value: "https://static.crates.io/crates/rusb/rusb-0.9.4.crate"
+    primary_source: true
+  }
+  version: "0.9.4"
+  license_type: NOTICE
+  last_upgrade_date {
+    year: 2024
+    month: 11
+    day: 8
+  }
+}
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..ff26fee
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+include platform/prebuilts/rust:main:/OWNERS
[email protected]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b10746b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+# Rusb
+This crate provides a safe wrapper around the native `libusb` library. It applies the RAII pattern
+and Rust lifetimes to ensure safe usage of all `libusb` functionality. The RAII pattern ensures that
+all acquired resources are released when they're no longer needed, and Rust lifetimes ensure that
+resources are released in a proper order.
+
+* [Documentation](https://docs.rs/rusb)
+
+## Dependencies
+To use rusb no extra setup is required as rusb will automatically download the source for libusb and build it.
+
+However if building libusb fails you can also try setting up the native `libusb` library where it can
+be found by `pkg-config` or `vcpkg`.
+
+All systems supported by the native `libusb` library are also supported by the `libusb` crate. It's
+been tested on Linux, OS X, and Windows.
+
+### Cross-Compiling
+The `rusb` crate can be used when cross-compiling to a foreign target. Details on how to
+cross-compile `rusb` are explained in the [`libusb1-sys` crate's
+README](libusb1-sys/README.md#cross-compiling).
+
+## Usage
+Add `rusb` as a dependency in `Cargo.toml`:
+
+```toml
+[dependencies]
+rusb = "0.9"
+```
+
+Import the `rusb` crate. The starting point for nearly all `rusb` functionality is to create a
+context object. With a context object, you can list devices, read their descriptors, open them, and
+communicate with their endpoints:
+
+```rust
+fn main() {
+    for device in rusb::devices().unwrap().iter() {
+        let device_desc = device.device_descriptor().unwrap();
+
+        println!("Bus {:03} Device {:03} ID {:04x}:{:04x}",
+            device.bus_number(),
+            device.address(),
+            device_desc.vendor_id(),
+            device_desc.product_id());
+    }
+}
+```
+
+## License
+Distributed under the [MIT License](LICENSE).
+
+### License note.
+If you link native `libusb` (by example using `vendored` features) library statically then
+you must follow [GNU LGPL](https://github.com/libusb/libusb/blob/master/COPYING) from libusb.
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..9d65603
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,32 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+
+fn get_api_version(libusb_source: &Path) {
+    use std::io::BufRead;
+    if let Ok(f) = fs::File::open(libusb_source) {
+        let f = std::io::BufReader::new(f);
+        for line in f.lines().flatten() {
+            if line.starts_with("#define LIBUSB_API_VERSION") {
+                if let Some(api_version) = line.rsplit(' ').next().and_then(|s| {
+                    if let Some(s) = s.strip_prefix("0x") {
+                        u32::from_str_radix(s, 16).ok()
+                    } else {
+                        None
+                    }
+                }) {
+                    if api_version >= 0x01000108 {
+                        println!("cargo:rustc-cfg=libusb_hotplug_get_user_data");
+                    }
+                }
+                break;
+            }
+        }
+    }
+}
+
+fn main() {
+    if let Ok(include_path) = std::env::var("DEP_USB_1.0_INCLUDE") {
+        let path = PathBuf::from(include_path);
+        get_api_version(path.join("libusb.h").as_path());
+    }
+}
diff --git a/cargo_embargo.json b/cargo_embargo.json
new file mode 100644
index 0000000..311c48a
--- /dev/null
+++ b/cargo_embargo.json
@@ -0,0 +1,5 @@
+{
+  "tests": false,
+  "run_cargo": false,
+  "product_available": false
+}
diff --git a/examples/hotplug.rs b/examples/hotplug.rs
new file mode 100644
index 0000000..7d0f023
--- /dev/null
+++ b/examples/hotplug.rs
@@ -0,0 +1,43 @@
+use rusb::{Context, Device, HotplugBuilder, UsbContext};
+
+struct HotPlugHandler;
+
+impl<T: UsbContext> rusb::Hotplug<T> for HotPlugHandler {
+    fn device_arrived(&mut self, device: Device<T>) {
+        println!("device arrived {:?}", device);
+    }
+
+    fn device_left(&mut self, device: Device<T>) {
+        println!("device left {:?}", device);
+    }
+}
+
+impl Drop for HotPlugHandler {
+    fn drop(&mut self) {
+        println!("HotPlugHandler dropped");
+    }
+}
+
+fn main() -> rusb::Result<()> {
+    if rusb::has_hotplug() {
+        let context = Context::new()?;
+
+        let mut reg = Some(
+            HotplugBuilder::new()
+                .enumerate(true)
+                .register(&context, Box::new(HotPlugHandler {}))?,
+        );
+
+        loop {
+            context.handle_events(None).unwrap();
+            if let Some(reg) = reg.take() {
+                context.unregister_callback(reg);
+                break;
+            }
+        }
+        Ok(())
+    } else {
+        eprint!("libusb hotplug api unsupported");
+        Ok(())
+    }
+}
diff --git a/examples/libusb_info.rs b/examples/libusb_info.rs
new file mode 100644
index 0000000..2465306
--- /dev/null
+++ b/examples/libusb_info.rs
@@ -0,0 +1,33 @@
+use rusb::UsbContext;
+
+fn main() {
+    let version = rusb::version();
+
+    println!(
+        "libusb v{}.{}.{}.{}{}",
+        version.major(),
+        version.minor(),
+        version.micro(),
+        version.nano(),
+        version.rc().unwrap_or("")
+    );
+
+    let mut context = match rusb::Context::new() {
+        Ok(c) => c,
+        Err(e) => panic!("libusb::Context::new(): {}", e),
+    };
+
+    context.set_log_level(rusb::LogLevel::Debug);
+    context.set_log_level(rusb::LogLevel::Info);
+    context.set_log_level(rusb::LogLevel::Warning);
+    context.set_log_level(rusb::LogLevel::Error);
+    context.set_log_level(rusb::LogLevel::None);
+
+    println!("has capability? {}", rusb::has_capability());
+    println!("has hotplug? {}", rusb::has_hotplug());
+    println!("has HID access? {}", rusb::has_hid_access());
+    println!(
+        "supports detach kernel driver? {}",
+        rusb::supports_detach_kernel_driver()
+    )
+}
diff --git a/examples/list_devices.rs b/examples/list_devices.rs
new file mode 100644
index 0000000..1ae4950
--- /dev/null
+++ b/examples/list_devices.rs
@@ -0,0 +1,278 @@
+use rusb::{
+    ConfigDescriptor, DeviceDescriptor, DeviceHandle, DeviceList, EndpointDescriptor,
+    InterfaceDescriptor, Language, Result, Speed, UsbContext,
+};
+use std::time::Duration;
+
+use usb_ids::{self, FromId};
+
+struct UsbDevice<T: UsbContext> {
+    handle: DeviceHandle<T>,
+    language: Language,
+    timeout: Duration,
+}
+
+fn main() {
+    list_devices().unwrap();
+}
+
+fn list_devices() -> Result<()> {
+    let timeout = Duration::from_secs(1);
+
+    for device in DeviceList::new()?.iter() {
+        let device_desc = match device.device_descriptor() {
+            Ok(d) => d,
+            Err(_) => continue,
+        };
+
+        let mut usb_device = {
+            match device.open() {
+                Ok(h) => match h.read_languages(timeout) {
+                    Ok(l) => {
+                        if !l.is_empty() {
+                            Some(UsbDevice {
+                                handle: h,
+                                language: l[0],
+                                timeout,
+                            })
+                        } else {
+                            None
+                        }
+                    }
+                    Err(_) => None,
+                },
+                Err(_) => None,
+            }
+        };
+
+        println!(
+            "Bus {:03} Device {:03} ID {:04x}:{:04x} {}",
+            device.bus_number(),
+            device.address(),
+            device_desc.vendor_id(),
+            device_desc.product_id(),
+            get_speed(device.speed())
+        );
+        print_device(&device_desc, &mut usb_device);
+
+        for n in 0..device_desc.num_configurations() {
+            let config_desc = match device.config_descriptor(n) {
+                Ok(c) => c,
+                Err(_) => continue,
+            };
+
+            print_config(&config_desc, &mut usb_device);
+
+            for interface in config_desc.interfaces() {
+                for interface_desc in interface.descriptors() {
+                    print_interface(&interface_desc, &mut usb_device);
+
+                    for endpoint_desc in interface_desc.endpoint_descriptors() {
+                        print_endpoint(&endpoint_desc);
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
+fn print_device<T: UsbContext>(device_desc: &DeviceDescriptor, handle: &mut Option<UsbDevice<T>>) {
+    let vid = device_desc.vendor_id();
+    let pid = device_desc.product_id();
+
+    let vendor_name = match usb_ids::Vendor::from_id(device_desc.vendor_id()) {
+        Some(vendor) => vendor.name(),
+        None => "Unknown vendor",
+    };
+
+    let product_name =
+        match usb_ids::Device::from_vid_pid(device_desc.vendor_id(), device_desc.product_id()) {
+            Some(product) => product.name(),
+            None => "Unknown product",
+        };
+
+    println!("Device Descriptor:");
+    println!("  bLength              {:3}", device_desc.length());
+    println!("  bDescriptorType      {:3}", device_desc.descriptor_type());
+    println!(
+        "  bcdUSB             {:2}.{}{}",
+        device_desc.usb_version().major(),
+        device_desc.usb_version().minor(),
+        device_desc.usb_version().sub_minor()
+    );
+    println!("  bDeviceClass        {:#04x}", device_desc.class_code());
+    println!(
+        "  bDeviceSubClass     {:#04x}",
+        device_desc.sub_class_code()
+    );
+    println!("  bDeviceProtocol     {:#04x}", device_desc.protocol_code());
+    println!("  bMaxPacketSize0      {:3}", device_desc.max_packet_size());
+    println!("  idVendor          {vid:#06x} {vendor_name}",);
+    println!("  idProduct         {pid:#06x} {product_name}",);
+    println!(
+        "  bcdDevice          {:2}.{}{}",
+        device_desc.device_version().major(),
+        device_desc.device_version().minor(),
+        device_desc.device_version().sub_minor()
+    );
+    println!(
+        "  iManufacturer        {:3} {}",
+        device_desc.manufacturer_string_index().unwrap_or(0),
+        handle.as_mut().map_or(String::new(), |h| h
+            .handle
+            .read_manufacturer_string(h.language, device_desc, h.timeout)
+            .unwrap_or_default())
+    );
+    println!(
+        "  iProduct             {:3} {}",
+        device_desc.product_string_index().unwrap_or(0),
+        handle.as_mut().map_or(String::new(), |h| h
+            .handle
+            .read_product_string(h.language, device_desc, h.timeout)
+            .unwrap_or_default())
+    );
+    println!(
+        "  iSerialNumber        {:3} {}",
+        device_desc.serial_number_string_index().unwrap_or(0),
+        handle.as_mut().map_or(String::new(), |h| h
+            .handle
+            .read_serial_number_string(h.language, device_desc, h.timeout)
+            .unwrap_or_default())
+    );
+    println!(
+        "  bNumConfigurations   {:3}",
+        device_desc.num_configurations()
+    );
+}
+
+fn print_config<T: UsbContext>(config_desc: &ConfigDescriptor, handle: &mut Option<UsbDevice<T>>) {
+    println!("  Config Descriptor:");
+    println!("    bLength              {:3}", config_desc.length());
+    println!(
+        "    bDescriptorType      {:3}",
+        config_desc.descriptor_type()
+    );
+    println!("    wTotalLength      {:#06x}", config_desc.total_length());
+    println!(
+        "    bNumInterfaces       {:3}",
+        config_desc.num_interfaces()
+    );
+    println!("    bConfigurationValue  {:3}", config_desc.number());
+    println!(
+        "    iConfiguration       {:3} {}",
+        config_desc.description_string_index().unwrap_or(0),
+        handle.as_mut().map_or(String::new(), |h| h
+            .handle
+            .read_configuration_string(h.language, config_desc, h.timeout)
+            .unwrap_or_default())
+    );
+    println!("    bmAttributes:");
+    println!("      Self Powered     {:>5}", config_desc.self_powered());
+    println!("      Remote Wakeup    {:>5}", config_desc.remote_wakeup());
+    println!("    bMaxPower           {:4}mW", config_desc.max_power());
+
+    if !config_desc.extra().is_empty() {
+        println!("    {:?}", config_desc.extra());
+    } else {
+        println!("    no extra data");
+    }
+}
+
+fn print_interface<T: UsbContext>(
+    interface_desc: &InterfaceDescriptor,
+    handle: &mut Option<UsbDevice<T>>,
+) {
+    println!("    Interface Descriptor:");
+    println!("      bLength              {:3}", interface_desc.length());
+    println!(
+        "      bDescriptorType      {:3}",
+        interface_desc.descriptor_type()
+    );
+    println!(
+        "      bInterfaceNumber     {:3}",
+        interface_desc.interface_number()
+    );
+    println!(
+        "      bAlternateSetting    {:3}",
+        interface_desc.setting_number()
+    );
+    println!(
+        "      bNumEndpoints        {:3}",
+        interface_desc.num_endpoints()
+    );
+    println!(
+        "      bInterfaceClass     {:#04x}",
+        interface_desc.class_code()
+    );
+    println!(
+        "      bInterfaceSubClass  {:#04x}",
+        interface_desc.sub_class_code()
+    );
+    println!(
+        "      bInterfaceProtocol  {:#04x}",
+        interface_desc.protocol_code()
+    );
+    println!(
+        "      iInterface           {:3} {}",
+        interface_desc.description_string_index().unwrap_or(0),
+        handle.as_mut().map_or(String::new(), |h| h
+            .handle
+            .read_interface_string(h.language, interface_desc, h.timeout)
+            .unwrap_or_default())
+    );
+
+    if interface_desc.extra().is_empty() {
+        println!("    {:?}", interface_desc.extra());
+    } else {
+        println!("    no extra data");
+    }
+}
+
+fn print_endpoint(endpoint_desc: &EndpointDescriptor) {
+    println!("      Endpoint Descriptor:");
+    println!("        bLength              {:3}", endpoint_desc.length());
+    println!(
+        "        bDescriptorType      {:3}",
+        endpoint_desc.descriptor_type()
+    );
+    println!(
+        "        bEndpointAddress    {:#04x} EP {} {:?}",
+        endpoint_desc.address(),
+        endpoint_desc.number(),
+        endpoint_desc.direction()
+    );
+    println!("        bmAttributes:");
+    println!(
+        "          Transfer Type          {:?}",
+        endpoint_desc.transfer_type()
+    );
+    println!(
+        "          Synch Type             {:?}",
+        endpoint_desc.sync_type()
+    );
+    println!(
+        "          Usage Type             {:?}",
+        endpoint_desc.usage_type()
+    );
+    println!(
+        "        wMaxPacketSize    {:#06x}",
+        endpoint_desc.max_packet_size()
+    );
+    println!(
+        "        bInterval            {:3}",
+        endpoint_desc.interval()
+    );
+}
+
+fn get_speed(speed: Speed) -> &'static str {
+    match speed {
+        Speed::SuperPlus => "10000 Mbps",
+        Speed::Super => "5000 Mbps",
+        Speed::High => " 480 Mbps",
+        Speed::Full => "  12 Mbps",
+        Speed::Low => " 1.5 Mbps",
+        _ => "(unknown)",
+    }
+}
diff --git a/examples/read_device.rs b/examples/read_device.rs
new file mode 100644
index 0000000..bf1d943
--- /dev/null
+++ b/examples/read_device.rs
@@ -0,0 +1,209 @@
+use std::time::Duration;
+
+use rusb::{
+    Context, Device, DeviceDescriptor, DeviceHandle, Direction, Result, TransferType, UsbContext,
+};
+
+#[derive(Debug)]
+struct Endpoint {
+    config: u8,
+    iface: u8,
+    setting: u8,
+    address: u8,
+}
+
+fn convert_argument(input: &str) -> u16 {
+    if input.starts_with("0x") {
+        return u16::from_str_radix(input.trim_start_matches("0x"), 16).unwrap();
+    }
+    u16::from_str_radix(input, 10)
+        .expect("Invalid input, be sure to add `0x` for hexadecimal values.")
+}
+
+fn main() {
+    let args: Vec<String> = std::env::args().collect();
+
+    if args.len() < 3 {
+        println!("usage: read_device <base-10/0xbase-16> <base-10/0xbase-16>");
+        return;
+    }
+
+    let vid = convert_argument(args[1].as_ref());
+    let pid = convert_argument(args[2].as_ref());
+
+    match Context::new() {
+        Ok(mut context) => match open_device(&mut context, vid, pid) {
+            Some((mut device, device_desc, mut handle)) => {
+                read_device(&mut device, &device_desc, &mut handle).unwrap()
+            }
+            None => println!("could not find device {:04x}:{:04x}", vid, pid),
+        },
+        Err(e) => panic!("could not initialize libusb: {}", e),
+    }
+}
+
+fn open_device<T: UsbContext>(
+    context: &mut T,
+    vid: u16,
+    pid: u16,
+) -> Option<(Device<T>, DeviceDescriptor, DeviceHandle<T>)> {
+    let devices = match context.devices() {
+        Ok(d) => d,
+        Err(_) => return None,
+    };
+
+    for device in devices.iter() {
+        let device_desc = match device.device_descriptor() {
+            Ok(d) => d,
+            Err(_) => continue,
+        };
+
+        if device_desc.vendor_id() == vid && device_desc.product_id() == pid {
+            match device.open() {
+                Ok(handle) => return Some((device, device_desc, handle)),
+                Err(e) => panic!("Device found but failed to open: {}", e),
+            }
+        }
+    }
+
+    None
+}
+
+fn read_device<T: UsbContext>(
+    device: &mut Device<T>,
+    device_desc: &DeviceDescriptor,
+    handle: &mut DeviceHandle<T>,
+) -> Result<()> {
+    handle.reset()?;
+
+    let timeout = Duration::from_secs(1);
+    let languages = handle.read_languages(timeout)?;
+
+    println!("Active configuration: {}", handle.active_configuration()?);
+    println!("Languages: {:?}", languages);
+
+    if !languages.is_empty() {
+        let language = languages[0];
+
+        println!(
+            "Manufacturer: {:?}",
+            handle
+                .read_manufacturer_string(language, device_desc, timeout)
+                .ok()
+        );
+        println!(
+            "Product: {:?}",
+            handle
+                .read_product_string(language, device_desc, timeout)
+                .ok()
+        );
+        println!(
+            "Serial Number: {:?}",
+            handle
+                .read_serial_number_string(language, device_desc, timeout)
+                .ok()
+        );
+    }
+
+    match find_readable_endpoint(device, device_desc, TransferType::Interrupt) {
+        Some(endpoint) => read_endpoint(handle, endpoint, TransferType::Interrupt),
+        None => println!("No readable interrupt endpoint"),
+    }
+
+    match find_readable_endpoint(device, device_desc, TransferType::Bulk) {
+        Some(endpoint) => read_endpoint(handle, endpoint, TransferType::Bulk),
+        None => println!("No readable bulk endpoint"),
+    }
+
+    Ok(())
+}
+
+fn find_readable_endpoint<T: UsbContext>(
+    device: &mut Device<T>,
+    device_desc: &DeviceDescriptor,
+    transfer_type: TransferType,
+) -> Option<Endpoint> {
+    for n in 0..device_desc.num_configurations() {
+        let config_desc = match device.config_descriptor(n) {
+            Ok(c) => c,
+            Err(_) => continue,
+        };
+
+        for interface in config_desc.interfaces() {
+            for interface_desc in interface.descriptors() {
+                for endpoint_desc in interface_desc.endpoint_descriptors() {
+                    if endpoint_desc.direction() == Direction::In
+                        && endpoint_desc.transfer_type() == transfer_type
+                    {
+                        return Some(Endpoint {
+                            config: config_desc.number(),
+                            iface: interface_desc.interface_number(),
+                            setting: interface_desc.setting_number(),
+                            address: endpoint_desc.address(),
+                        });
+                    }
+                }
+            }
+        }
+    }
+
+    None
+}
+
+fn read_endpoint<T: UsbContext>(
+    handle: &mut DeviceHandle<T>,
+    endpoint: Endpoint,
+    transfer_type: TransferType,
+) {
+    println!("Reading from endpoint: {:?}", endpoint);
+
+    let has_kernel_driver = match handle.kernel_driver_active(endpoint.iface) {
+        Ok(true) => {
+            handle.detach_kernel_driver(endpoint.iface).ok();
+            true
+        }
+        _ => false,
+    };
+
+    println!(" - kernel driver? {}", has_kernel_driver);
+
+    match configure_endpoint(handle, &endpoint) {
+        Ok(_) => {
+            let mut buf = [0; 256];
+            let timeout = Duration::from_secs(1);
+
+            match transfer_type {
+                TransferType::Interrupt => {
+                    match handle.read_interrupt(endpoint.address, &mut buf, timeout) {
+                        Ok(len) => {
+                            println!(" - read: {:?}", &buf[..len]);
+                        }
+                        Err(err) => println!("could not read from endpoint: {}", err),
+                    }
+                }
+                TransferType::Bulk => match handle.read_bulk(endpoint.address, &mut buf, timeout) {
+                    Ok(len) => {
+                        println!(" - read: {:?}", &buf[..len]);
+                    }
+                    Err(err) => println!("could not read from endpoint: {}", err),
+                },
+                _ => (),
+            }
+        }
+        Err(err) => println!("could not configure endpoint: {}", err),
+    }
+
+    if has_kernel_driver {
+        handle.attach_kernel_driver(endpoint.iface).ok();
+    }
+}
+
+fn configure_endpoint<T: UsbContext>(
+    handle: &mut DeviceHandle<T>,
+    endpoint: &Endpoint,
+) -> Result<()> {
+    handle.set_active_configuration(endpoint.config)?;
+    handle.claim_interface(endpoint.iface)?;
+    handle.set_alternate_setting(endpoint.iface, endpoint.setting)?;
+    Ok(())
+}
diff --git a/src/config_descriptor.rs b/src/config_descriptor.rs
new file mode 100644
index 0000000..7f66c11
--- /dev/null
+++ b/src/config_descriptor.rs
@@ -0,0 +1,255 @@
+use std::{fmt, slice};
+
+use libusb1_sys::*;
+
+use crate::interface_descriptor::{self, Interface};
+
+/// Describes a configuration.
+pub struct ConfigDescriptor {
+    descriptor: *const libusb_config_descriptor,
+}
+
+impl Drop for ConfigDescriptor {
+    fn drop(&mut self) {
+        unsafe {
+            libusb_free_config_descriptor(self.descriptor);
+        }
+    }
+}
+
+unsafe impl Sync for ConfigDescriptor {}
+unsafe impl Send for ConfigDescriptor {}
+
+impl ConfigDescriptor {
+    /// Returns the size of the descriptor in bytes
+    pub fn length(&self) -> u8 {
+        unsafe { (*self.descriptor).bLength }
+    }
+
+    /// Returns the total length in bytes of data returned for this configuration: all interfaces and endpoints
+    pub fn total_length(&self) -> u16 {
+        unsafe { (*self.descriptor).wTotalLength }
+    }
+
+    /// Returns the descriptor type
+    pub fn descriptor_type(&self) -> u8 {
+        unsafe { (*self.descriptor).bDescriptorType }
+    }
+
+    /// Returns the configuration number.
+    pub fn number(&self) -> u8 {
+        unsafe { (*self.descriptor).bConfigurationValue }
+    }
+
+    /// Returns the device's maximum power consumption (in milliamps) in this configuration.
+    pub fn max_power(&self) -> u16 {
+        unsafe { u16::from((*self.descriptor).bMaxPower) * 2 }
+    }
+
+    /// Indicates if the device is self-powered in this configuration.
+    pub fn self_powered(&self) -> bool {
+        unsafe { (*self.descriptor).bmAttributes & 0x40 != 0 }
+    }
+
+    /// Indicates if the device has remote wakeup capability in this configuration.
+    pub fn remote_wakeup(&self) -> bool {
+        unsafe { (*self.descriptor).bmAttributes & 0x20 != 0 }
+    }
+
+    /// Returns the index of the string descriptor that describes the configuration.
+    pub fn description_string_index(&self) -> Option<u8> {
+        unsafe {
+            match (*self.descriptor).iConfiguration {
+                0 => None,
+                n => Some(n),
+            }
+        }
+    }
+
+    /// Returns the number of interfaces for this configuration.
+    pub fn num_interfaces(&self) -> u8 {
+        unsafe { (*self.descriptor).bNumInterfaces }
+    }
+
+    /// Returns a collection of the configuration's interfaces.
+    pub fn interfaces(&self) -> Interfaces {
+        let interfaces = unsafe {
+            slice::from_raw_parts(
+                (*self.descriptor).interface,
+                (*self.descriptor).bNumInterfaces as usize,
+            )
+        };
+
+        Interfaces {
+            iter: interfaces.iter(),
+        }
+    }
+
+    /// Returns the unknown 'extra' bytes that libusb does not understand.
+    pub fn extra(&self) -> &[u8] {
+        unsafe {
+            match (*self.descriptor).extra_length {
+                len if len > 0 => slice::from_raw_parts((*self.descriptor).extra, len as usize),
+                _ => &[],
+            }
+        }
+    }
+}
+
+impl fmt::Debug for ConfigDescriptor {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        let mut debug = fmt.debug_struct("ConfigDescriptor");
+
+        let descriptor: &libusb_config_descriptor = unsafe { &*self.descriptor };
+
+        debug.field("bLength", &descriptor.bLength);
+        debug.field("bDescriptorType", &descriptor.bDescriptorType);
+        debug.field("wTotalLength", &descriptor.wTotalLength);
+        debug.field("bNumInterfaces", &descriptor.bNumInterfaces);
+        debug.field("bConfigurationValue", &descriptor.bConfigurationValue);
+        debug.field("iConfiguration", &descriptor.iConfiguration);
+        debug.field("bmAttributes", &descriptor.bmAttributes);
+        debug.field("bMaxPower", &descriptor.bMaxPower);
+        debug.field("extra", &self.extra());
+
+        debug.finish()
+    }
+}
+
+/// Iterator over a configuration's interfaces.
+pub struct Interfaces<'a> {
+    iter: slice::Iter<'a, libusb_interface>,
+}
+
+impl<'a> Iterator for Interfaces<'a> {
+    type Item = Interface<'a>;
+
+    fn next(&mut self) -> Option<Interface<'a>> {
+        self.iter
+            .next()
+            .map(|interface| unsafe { interface_descriptor::from_libusb(interface) })
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.iter.size_hint()
+    }
+}
+
+#[doc(hidden)]
+pub(crate) unsafe fn from_libusb(config: *const libusb_config_descriptor) -> ConfigDescriptor {
+    ConfigDescriptor { descriptor: config }
+}
+
+#[cfg(test)]
+mod test {
+    use std::mem::ManuallyDrop;
+
+    // The Drop trait impl calls libusb_free_config_descriptor(), which would attempt to free
+    // unallocated memory for a stack-allocated config descriptor. Allocating a config descriptor
+    // is not a simple malloc()/free() inside libusb. Mimicking libusb's allocation would be
+    // error-prone, difficult to maintain, and provide little benefit for the tests. It's easier to
+    // use mem::forget() to prevent the Drop trait impl from running. The config descriptor passed
+    // as `$config` should be stack-allocated to prevent memory leaks in the test suite.
+    macro_rules! with_config {
+        ($name:ident : $config:expr => $body:block) => {{
+            let config = $config;
+            let $name = ManuallyDrop::new(unsafe { super::from_libusb(&config) });
+            $body;
+        }};
+    }
+
+    #[test]
+    fn it_has_number() {
+        with_config!(config: config_descriptor!(bConfigurationValue: 42) => {
+            assert_eq!(42, config.number());
+        });
+    }
+
+    #[test]
+    fn it_has_max_power() {
+        with_config!(config: config_descriptor!(bMaxPower: 21) => {
+            assert_eq!(42, config.max_power());
+        });
+    }
+
+    #[test]
+    fn it_interprets_self_powered_bit_in_attributes() {
+        with_config!(config: config_descriptor!(bmAttributes: 0b0000_0000) => {
+            assert_eq!(false, config.self_powered());
+        });
+
+        with_config!(config: config_descriptor!(bmAttributes: 0b0100_0000) => {
+            assert_eq!(true, config.self_powered());
+        });
+    }
+
+    #[test]
+    fn it_interprets_remote_wakeup_bit_in_attributes() {
+        with_config!(config: config_descriptor!(bmAttributes: 0b0000_0000) => {
+            assert_eq!(false, config.remote_wakeup());
+        });
+
+        with_config!(config: config_descriptor!(bmAttributes: 0b0010_0000) => {
+            assert_eq!(true, config.remote_wakeup());
+        });
+    }
+
+    #[test]
+    fn it_has_description_string_index() {
+        with_config!(config: config_descriptor!(iConfiguration: 42) => {
+            assert_eq!(Some(42), config.description_string_index());
+        });
+    }
+
+    #[test]
+    fn it_handles_missing_description_string_index() {
+        with_config!(config: config_descriptor!(iConfiguration: 0) => {
+            assert_eq!(None, config.description_string_index());
+        });
+    }
+
+    #[test]
+    fn it_has_num_interfaces() {
+        let interface1 = interface!(interface_descriptor!(bInterfaceNumber: 1));
+        let interface2 = interface!(interface_descriptor!(bInterfaceNumber: 2));
+
+        with_config!(config: config_descriptor!(interface1, interface2) => {
+            assert_eq!(2, config.num_interfaces());
+        });
+    }
+
+    #[test]
+    fn it_has_interfaces() {
+        let interface = interface!(interface_descriptor!(bInterfaceNumber: 1));
+
+        with_config!(config: config_descriptor!(interface) => {
+            let interface_numbers = config.interfaces().map(|interface| {
+                interface.number()
+            }).collect::<Vec<_>>();
+
+            assert_eq!(vec![1], interface_numbers);
+        });
+    }
+
+    // Successful compilation shows that the lifetime of the endpoint descriptor(s) is the same
+    // as the lifetime of the config descriptor.
+    #[test]
+    fn it_had_interfaces_with_endpoints() {
+        let endpoint1 = endpoint_descriptor!(bEndpointAddress: 0x81);
+        let endpoint2 = endpoint_descriptor!(bEndpointAddress: 0x01);
+        let endpoint3 = endpoint_descriptor!(bEndpointAddress: 0x02);
+        let interface1 = interface!(interface_descriptor!(endpoint1, endpoint2));
+        let interface2 = interface!(interface_descriptor!(endpoint3));
+
+        with_config!(config: config_descriptor!(interface1, interface2) => {
+            // Exists only to name config's lifetime.
+            fn named_lifetime<'a>(config: &'a super::ConfigDescriptor) {
+                let addresses: Vec<_> = config.interfaces().flat_map(|intf| intf.descriptors()).flat_map(|desc| desc.endpoint_descriptors()).map(|ep| ep.address()).collect();
+                assert_eq!(addresses, &[0x81, 0x01, 0x02]);
+                let desc: crate::InterfaceDescriptor<'a> = config.interfaces().flat_map(|intf| intf.descriptors()).next().expect("There's one interface");
+                let _: crate::EndpointDescriptor<'a> = desc.endpoint_descriptors().next().expect("There's one endpoint");
+            }
+            named_lifetime(&*config);
+        })
+    }
+}
diff --git a/src/context.rs b/src/context.rs
new file mode 100644
index 0000000..2add6b3
--- /dev/null
+++ b/src/context.rs
@@ -0,0 +1,373 @@
+use libc::{c_char, c_int, c_void, timeval};
+
+use std::{
+    cmp::Ordering, ffi::CStr, mem, ptr, sync::Arc, sync::Mutex, sync::Once, sync::OnceLock,
+    time::Duration,
+};
+
+#[cfg(unix)]
+use std::os::unix::io::RawFd;
+
+use crate::hotplug::{Hotplug, HotplugBuilder, Registration};
+use crate::{device_handle::DeviceHandle, device_list::DeviceList, error};
+use libusb1_sys::{constants::*, *};
+
+#[cfg(windows)]
+type Seconds = ::libc::c_long;
+#[cfg(windows)]
+type MicroSeconds = ::libc::c_long;
+
+#[cfg(not(windows))]
+type Seconds = ::libc::time_t;
+#[cfg(not(windows))]
+type MicroSeconds = ::libc::suseconds_t;
+
+#[derive(Copy, Clone, Eq, PartialEq, Default)]
+pub struct GlobalContext {}
+
+/// A `libusb` context.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Context {
+    context: Arc<ContextInner>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+struct ContextInner {
+    inner: ptr::NonNull<libusb_context>,
+}
+
+impl Drop for ContextInner {
+    /// Closes the `libusb` context.
+    fn drop(&mut self) {
+        unsafe {
+            libusb_exit(self.inner.as_ptr());
+        }
+    }
+}
+
+unsafe impl Sync for Context {}
+unsafe impl Send for Context {}
+
+type LogCallback = Box<dyn Fn(LogLevel, String)>;
+
+struct LogCallbackMap {
+    map: std::collections::HashMap<*mut libusb_context, LogCallback>,
+}
+
+unsafe impl Sync for LogCallbackMap {}
+unsafe impl Send for LogCallbackMap {}
+
+impl LogCallbackMap {
+    pub fn new() -> Self {
+        Self {
+            map: std::collections::HashMap::new(),
+        }
+    }
+}
+
+static LOG_CALLBACK_MAP: OnceLock<Mutex<LogCallbackMap>> = OnceLock::new();
+
+extern "system" fn static_log_callback(
+    context: *mut libusb_context,
+    level: c_int,
+    text: *mut c_void,
+) {
+    if let Some(log_callback_map) = LOG_CALLBACK_MAP.get() {
+        if let Ok(locked_table) = log_callback_map.lock() {
+            if let Some(logger) = locked_table.map.get(&context) {
+                let c_str: &CStr = unsafe { CStr::from_ptr(text as *const c_char) };
+                let str_slice: &str = c_str.to_str().unwrap_or("");
+                let log_message = str_slice.to_owned();
+
+                logger(LogLevel::from_c_int(level), log_message);
+            }
+        }
+    }
+}
+
+pub trait UsbContext: Clone + Sized + Send + Sync {
+    /// Get the raw libusb_context pointer, for advanced use in unsafe code.
+    fn as_raw(&self) -> *mut libusb_context;
+
+    /// Returns a list of the current USB devices.
+    fn devices(&self) -> crate::Result<DeviceList<Self>> {
+        DeviceList::new_with_context(self.clone())
+    }
+
+    /// Convenience function to open a device by its vendor ID and product ID.
+    ///
+    /// This function is provided as a convenience for building prototypes without having to
+    /// iterate a [`DeviceList`](struct.DeviceList.html). It is not meant for production
+    /// applications.
+    ///
+    /// Returns a device handle for the first device found matching `vendor_id` and `product_id`.
+    /// On error, or if the device could not be found, it returns `None`.
+    fn open_device_with_vid_pid(
+        &self,
+        vendor_id: u16,
+        product_id: u16,
+    ) -> Option<DeviceHandle<Self>> {
+        let handle =
+            unsafe { libusb_open_device_with_vid_pid(self.as_raw(), vendor_id, product_id) };
+        let ptr = std::ptr::NonNull::new(handle)?;
+        Some(unsafe { DeviceHandle::from_libusb(self.clone(), ptr) })
+    }
+
+    /// Opens the device with a pre-opened file descriptor.
+    ///
+    /// This is UNIX-only and platform-specific. It is currently working with
+    /// Linux/Android, but might work with other systems in the future.
+    ///
+    /// Note: This function does not take ownership of the specified file
+    /// descriptor. The caller has the responsibility of keeping it opened for
+    /// as long as the device handle.
+    #[cfg(unix)]
+    #[doc(alias = "libusb_wrap_sys_device")]
+    unsafe fn open_device_with_fd(&self, fd: RawFd) -> crate::Result<DeviceHandle<Self>> {
+        let mut handle = mem::MaybeUninit::<*mut libusb_device_handle>::uninit();
+
+        match libusb_wrap_sys_device(self.as_raw(), fd as _, handle.as_mut_ptr()) {
+            0 => {
+                let ptr =
+                    std::ptr::NonNull::new(handle.assume_init()).ok_or(crate::Error::NoDevice)?;
+
+                Ok(DeviceHandle::from_libusb(self.clone(), ptr))
+            }
+            err => Err(error::from_libusb(err)),
+        }
+    }
+
+    /// Sets the log level of a `libusb` for context.
+    fn set_log_level(&mut self, level: LogLevel) {
+        unsafe {
+            libusb_set_debug(self.as_raw(), level.as_c_int());
+        }
+    }
+
+    fn set_log_callback(&mut self, log_callback: LogCallback, mode: LogCallbackMode) {
+        let log_callback_map = LOG_CALLBACK_MAP.get_or_init(|| Mutex::new(LogCallbackMap::new()));
+        if let Ok(mut locked_table) = log_callback_map.lock() {
+            locked_table.map.insert(self.as_raw(), log_callback);
+        }
+
+        unsafe {
+            libusb_set_log_cb(self.as_raw(), Some(static_log_callback), mode.as_c_int());
+        }
+    }
+
+    /// Register a callback to be called on hotplug events. The callback's
+    /// [Hotplug::device_arrived] method is called when a new device is added to
+    /// the bus, and [Hotplug::device_left] is called when it is removed.
+    ///
+    /// Devices can optionally be filtered by vendor (`vendor_id`) and device id
+    /// (`product_id`).
+    ///
+    /// The callback will remain registered until the returned [Registration] is
+    /// dropped, which can be done explicitly with [Context::unregister_callback].
+    ///
+    /// When handling a [Hotplug::device_arrived] event it is considered safe to call
+    /// any `rusb` function that takes a [crate::Device]. It also safe to open a device and
+    /// submit **asynchronous** transfers.
+    /// However, most other functions that take a [DeviceHandle] are **not safe** to call.
+    /// Examples of such functions are any of the synchronous API functions or
+    /// the blocking functions that retrieve various USB descriptors.
+    /// These functions must be used outside of the context of the [Hotplug] functions.
+    #[deprecated(since = "0.9.0", note = "Use HotplugBuilder")]
+    fn register_callback(
+        &self,
+        vendor_id: Option<u16>,
+        product_id: Option<u16>,
+        class: Option<u8>,
+        callback: Box<dyn Hotplug<Self>>,
+    ) -> crate::Result<Registration<Self>> {
+        let mut builder = HotplugBuilder::new();
+
+        let mut builder = &mut builder;
+        if let Some(vendor_id) = vendor_id {
+            builder = builder.vendor_id(vendor_id)
+        }
+        if let Some(product_id) = product_id {
+            builder = builder.product_id(product_id)
+        }
+        if let Some(class) = class {
+            builder = builder.class(class)
+        }
+
+        builder.register(self, callback)
+    }
+
+    /// Unregisters the callback corresponding to the given registration. The
+    /// same thing can be achieved by dropping the registration.
+    fn unregister_callback(&self, _reg: Registration<Self>) {}
+
+    /// Handle any pending events.
+    /// If timeout less then 1 microseconds then this function will handle any already-pending
+    /// events and then immediately return in non-blocking style.
+    /// If timeout is [None] then function will handle any pending events in blocking mode.
+    fn handle_events(&self, timeout: Option<Duration>) -> crate::Result<()> {
+        let n = unsafe {
+            match timeout {
+                Some(t) => {
+                    let tv = timeval {
+                        tv_sec: t.as_secs() as Seconds,
+                        tv_usec: t.subsec_nanos() as MicroSeconds / 1000,
+                    };
+                    libusb_handle_events_timeout_completed(self.as_raw(), &tv, ptr::null_mut())
+                }
+                None => libusb_handle_events_completed(self.as_raw(), ptr::null_mut()),
+            }
+        };
+        if n < 0 {
+            Err(error::from_libusb(n as c_int))
+        } else {
+            Ok(())
+        }
+    }
+
+    /// Interrupt any active thread that is handling events (for example with
+    /// [handle_events][`Self::handle_events()`]).
+    #[doc(alias = "libusb_interrupt_event_handler")]
+    fn interrupt_handle_events(&self) {
+        unsafe { libusb_interrupt_event_handler(self.as_raw()) }
+    }
+
+    fn next_timeout(&self) -> crate::Result<Option<Duration>> {
+        let mut tv = timeval {
+            tv_sec: 0,
+            tv_usec: 0,
+        };
+        let n = unsafe { libusb_get_next_timeout(self.as_raw(), &mut tv) };
+
+        match n.cmp(&0) {
+            Ordering::Less => Err(error::from_libusb(n as c_int)),
+            Ordering::Equal => Ok(None),
+            Ordering::Greater => {
+                let duration = Duration::new(tv.tv_sec as _, (tv.tv_usec * 1000) as _);
+                Ok(Some(duration))
+            }
+        }
+    }
+}
+
+impl UsbContext for Context {
+    fn as_raw(&self) -> *mut libusb_context {
+        self.context.inner.as_ptr()
+    }
+}
+
+impl UsbContext for GlobalContext {
+    fn as_raw(&self) -> *mut libusb_context {
+        static mut USB_CONTEXT: *mut libusb_context = ptr::null_mut();
+        static ONCE: Once = Once::new();
+
+        ONCE.call_once(|| {
+            let mut context = mem::MaybeUninit::<*mut libusb_context>::uninit();
+            unsafe {
+                USB_CONTEXT = match libusb_init(context.as_mut_ptr()) {
+                    0 => context.assume_init(),
+                    err => panic!(
+                        "Can't init Global usb context, error {:?}",
+                        error::from_libusb(err)
+                    ),
+                }
+            };
+        });
+        // Clone data that is safe to use concurrently.
+        unsafe { USB_CONTEXT }
+    }
+}
+
+impl Context {
+    /// Opens a new `libusb` context.
+    pub fn new() -> crate::Result<Self> {
+        let mut context = mem::MaybeUninit::<*mut libusb_context>::uninit();
+
+        try_unsafe!(libusb_init(context.as_mut_ptr()));
+
+        Ok(unsafe { Self::from_raw(context.assume_init()) })
+    }
+
+    /// Creates a new `libusb` context and sets runtime options.
+    pub fn with_options(opts: &[crate::UsbOption]) -> crate::Result<Self> {
+        let mut this = Self::new()?;
+
+        for opt in opts {
+            opt.apply(&mut this)?;
+        }
+
+        Ok(this)
+    }
+
+    /// Creates rusb Context from existing libusb context.
+    /// Note: This transfers ownership of the context to Rust.
+    /// # Safety
+    /// This is unsafe because it does not check if the context is valid,
+    /// so the caller must guarantee that libusb_context is created properly.
+    pub unsafe fn from_raw(raw: *mut libusb_context) -> Self {
+        Context {
+            context: Arc::new(ContextInner {
+                inner: ptr::NonNull::new_unchecked(raw),
+            }),
+        }
+    }
+}
+
+/// Library logging levels.
+#[derive(Clone, Copy)]
+pub enum LogLevel {
+    /// No messages are printed by `libusb` (default).
+    None,
+
+    /// Error messages printed to `stderr`.
+    Error,
+
+    /// Warning and error messages are printed to `stderr`.
+    Warning,
+
+    /// Informational messages are printed to `stdout`. Warnings and error messages are printed to
+    /// `stderr`.
+    Info,
+
+    /// Debug and informational messages are printed to `stdout`. Warnings and error messages are
+    /// printed to `stderr`.
+    Debug,
+}
+
+impl LogLevel {
+    pub(crate) fn as_c_int(self) -> c_int {
+        match self {
+            LogLevel::None => LIBUSB_LOG_LEVEL_NONE,
+            LogLevel::Error => LIBUSB_LOG_LEVEL_ERROR,
+            LogLevel::Warning => LIBUSB_LOG_LEVEL_WARNING,
+            LogLevel::Info => LIBUSB_LOG_LEVEL_INFO,
+            LogLevel::Debug => LIBUSB_LOG_LEVEL_DEBUG,
+        }
+    }
+
+    fn from_c_int(value: c_int) -> LogLevel {
+        match value {
+            LIBUSB_LOG_LEVEL_ERROR => LogLevel::Error,
+            LIBUSB_LOG_LEVEL_WARNING => LogLevel::Warning,
+            LIBUSB_LOG_LEVEL_INFO => LogLevel::Info,
+            LIBUSB_LOG_LEVEL_DEBUG => LogLevel::Debug,
+            _ => LogLevel::None,
+        }
+    }
+}
+
+pub enum LogCallbackMode {
+    /// Callback function handling all log messages.
+    Global,
+
+    /// Callback function handling context related log messages.
+    Context,
+}
+
+impl LogCallbackMode {
+    fn as_c_int(&self) -> c_int {
+        match *self {
+            LogCallbackMode::Global => LIBUSB_LOG_CB_GLOBAL,
+            LogCallbackMode::Context => LIBUSB_LOG_CB_CONTEXT,
+        }
+    }
+}
diff --git a/src/device.rs b/src/device.rs
new file mode 100644
index 0000000..af0042d
--- /dev/null
+++ b/src/device.rs
@@ -0,0 +1,178 @@
+use std::{
+    fmt::{self, Debug},
+    mem,
+    ptr::NonNull,
+};
+
+use libusb1_sys::*;
+
+use crate::{
+    config_descriptor::{self, ConfigDescriptor},
+    device_descriptor::{self, DeviceDescriptor},
+    device_handle::DeviceHandle,
+    error,
+    fields::{self, Speed},
+    Error, UsbContext,
+};
+
+/// A reference to a USB device.
+#[derive(Eq, PartialEq)]
+pub struct Device<T: UsbContext> {
+    context: T,
+    device: NonNull<libusb_device>,
+}
+
+impl<T: UsbContext> Drop for Device<T> {
+    /// Releases the device reference.
+    fn drop(&mut self) {
+        unsafe {
+            libusb_unref_device(self.device.as_ptr());
+        }
+    }
+}
+
+impl<T: UsbContext> Clone for Device<T> {
+    fn clone(&self) -> Self {
+        unsafe { Self::from_libusb(self.context.clone(), self.device) }
+    }
+}
+
+unsafe impl<T: UsbContext> Send for Device<T> {}
+unsafe impl<T: UsbContext> Sync for Device<T> {}
+
+impl<T: UsbContext> Debug for Device<T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let descriptor = match self.device_descriptor() {
+            Ok(descriptor) => descriptor,
+            Err(e) => {
+                return write!(f, "Can't read device descriptor {:?}", e);
+            }
+        };
+        write!(
+            f,
+            "Bus {:03} Device {:03}: ID {:04x}:{:04x}",
+            self.bus_number(),
+            self.address(),
+            descriptor.vendor_id(),
+            descriptor.product_id(),
+        )
+    }
+}
+
+impl<T: UsbContext> Device<T> {
+    /// Get the raw libusb_device pointer, for advanced use in unsafe code
+    pub fn as_raw(&self) -> *mut libusb_device {
+        self.device.as_ptr()
+    }
+
+    /// Get the context associated with this device
+    pub fn context(&self) -> &T {
+        &self.context
+    }
+
+    /// # Safety
+    ///
+    /// Converts an existing `libusb_device` pointer into a `Device<T>`.
+    /// `device` must be a pointer to a valid `libusb_device`. Rusb increments refcount.
+    pub unsafe fn from_libusb(context: T, device: NonNull<libusb_device>) -> Device<T> {
+        libusb_ref_device(device.as_ptr());
+
+        Device { context, device }
+    }
+
+    /// Reads the device descriptor.
+    pub fn device_descriptor(&self) -> crate::Result<DeviceDescriptor> {
+        let mut descriptor = mem::MaybeUninit::<libusb_device_descriptor>::uninit();
+
+        // since libusb 1.0.16, this function always succeeds
+        try_unsafe!(libusb_get_device_descriptor(
+            self.device.as_ptr(),
+            descriptor.as_mut_ptr()
+        ));
+
+        Ok(device_descriptor::from_libusb(unsafe {
+            descriptor.assume_init()
+        }))
+    }
+
+    /// Reads a configuration descriptor.
+    pub fn config_descriptor(&self, config_index: u8) -> crate::Result<ConfigDescriptor> {
+        let mut config = mem::MaybeUninit::<*const libusb_config_descriptor>::uninit();
+
+        try_unsafe!(libusb_get_config_descriptor(
+            self.device.as_ptr(),
+            config_index,
+            config.as_mut_ptr()
+        ));
+
+        Ok(unsafe { config_descriptor::from_libusb(config.assume_init()) })
+    }
+
+    /// Reads the configuration descriptor for the current configuration.
+    pub fn active_config_descriptor(&self) -> crate::Result<ConfigDescriptor> {
+        let mut config = mem::MaybeUninit::<*const libusb_config_descriptor>::uninit();
+
+        try_unsafe!(libusb_get_active_config_descriptor(
+            self.device.as_ptr(),
+            config.as_mut_ptr()
+        ));
+
+        Ok(unsafe { config_descriptor::from_libusb(config.assume_init()) })
+    }
+
+    /// Returns the number of the bus that the device is connected to.
+    pub fn bus_number(&self) -> u8 {
+        unsafe { libusb_get_bus_number(self.device.as_ptr()) }
+    }
+
+    /// Returns the device's address on the bus that it's connected to.
+    pub fn address(&self) -> u8 {
+        unsafe { libusb_get_device_address(self.device.as_ptr()) }
+    }
+
+    /// Returns the device's connection speed.
+    pub fn speed(&self) -> Speed {
+        fields::speed_from_libusb(unsafe { libusb_get_device_speed(self.device.as_ptr()) })
+    }
+
+    /// Opens the device.
+    pub fn open(&self) -> crate::Result<DeviceHandle<T>> {
+        let mut handle = mem::MaybeUninit::<*mut libusb_device_handle>::uninit();
+
+        try_unsafe!(libusb_open(self.device.as_ptr(), handle.as_mut_ptr()));
+
+        Ok(unsafe {
+            let ptr = NonNull::new(handle.assume_init()).ok_or(Error::NoDevice)?;
+            DeviceHandle::from_libusb(self.context.clone(), ptr)
+        })
+    }
+
+    /// Returns the device's port number
+    pub fn port_number(&self) -> u8 {
+        unsafe { libusb_get_port_number(self.device.as_ptr()) }
+    }
+
+    /// Returns the device's parent
+    pub fn get_parent(&self) -> Option<Self> {
+        let device = unsafe { libusb_get_parent(self.device.as_ptr()) };
+        NonNull::new(device)
+            .map(|device| unsafe { Device::from_libusb(self.context.clone(), device) })
+    }
+
+    ///  Get the list of all port numbers from root for the specified device
+    pub fn port_numbers(&self) -> Result<Vec<u8>, Error> {
+        // As per the USB 3.0 specs, the current maximum limit for the depth is 7.
+        let mut ports = [0; 7];
+
+        let result = unsafe {
+            libusb_get_port_numbers(self.device.as_ptr(), ports.as_mut_ptr(), ports.len() as i32)
+        };
+
+        let ports_number = if result < 0 {
+            return Err(error::from_libusb(result));
+        } else {
+            result
+        };
+        Ok(ports[0..ports_number as usize].to_vec())
+    }
+}
diff --git a/src/device_descriptor.rs b/src/device_descriptor.rs
new file mode 100644
index 0000000..70c4bed
--- /dev/null
+++ b/src/device_descriptor.rs
@@ -0,0 +1,244 @@
+use std::fmt;
+
+use libusb1_sys::*;
+
+use crate::fields::Version;
+
+/// Describes a device.
+pub struct DeviceDescriptor {
+    descriptor: libusb_device_descriptor,
+}
+
+impl DeviceDescriptor {
+    /// Returns the size of the descriptor in bytes
+    pub fn length(&self) -> u8 {
+        self.descriptor.bLength
+    }
+
+    /// Returns the descriptor type
+    pub fn descriptor_type(&self) -> u8 {
+        self.descriptor.bDescriptorType
+    }
+
+    /// Returns the device's maximum supported USB version.
+    pub fn usb_version(&self) -> Version {
+        Version::from_bcd(self.descriptor.bcdUSB)
+    }
+
+    /// Returns the manufacturer's version of the device.
+    pub fn device_version(&self) -> Version {
+        Version::from_bcd(self.descriptor.bcdDevice)
+    }
+
+    /// Returns the index of the string descriptor that contains the manufacturer name.
+    pub fn manufacturer_string_index(&self) -> Option<u8> {
+        match self.descriptor.iManufacturer {
+            0 => None,
+            n => Some(n),
+        }
+    }
+
+    /// Returns the index of the string descriptor that contains the product name.
+    pub fn product_string_index(&self) -> Option<u8> {
+        match self.descriptor.iProduct {
+            0 => None,
+            n => Some(n),
+        }
+    }
+
+    /// Returns the index of the string descriptor that contains the device's serial number.
+    pub fn serial_number_string_index(&self) -> Option<u8> {
+        match self.descriptor.iSerialNumber {
+            0 => None,
+            n => Some(n),
+        }
+    }
+
+    /// Returns the device's class code.
+    pub fn class_code(&self) -> u8 {
+        self.descriptor.bDeviceClass
+    }
+
+    /// Returns the device's sub class code.
+    pub fn sub_class_code(&self) -> u8 {
+        self.descriptor.bDeviceSubClass
+    }
+
+    /// Returns the device's protocol code.
+    pub fn protocol_code(&self) -> u8 {
+        self.descriptor.bDeviceProtocol
+    }
+
+    /// Returns the device's vendor ID.
+    pub fn vendor_id(&self) -> u16 {
+        self.descriptor.idVendor
+    }
+
+    /// Returns the device's product ID.
+    pub fn product_id(&self) -> u16 {
+        self.descriptor.idProduct
+    }
+
+    /// Returns the maximum packet size of the device's first endpoint.
+    pub fn max_packet_size(&self) -> u8 {
+        self.descriptor.bMaxPacketSize0
+    }
+
+    /// Returns the number of config descriptors available for the device.
+    pub fn num_configurations(&self) -> u8 {
+        self.descriptor.bNumConfigurations
+    }
+}
+
+impl fmt::Debug for DeviceDescriptor {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        let mut debug = fmt.debug_struct("DeviceDescriptor");
+
+        debug.field("bLength", &self.descriptor.bLength);
+        debug.field("bDescriptorType", &self.descriptor.bDescriptorType);
+        debug.field("bcdUSB", &self.descriptor.bcdUSB);
+        debug.field("bDeviceClass", &self.descriptor.bDeviceClass);
+        debug.field("bDeviceSubClass", &self.descriptor.bDeviceSubClass);
+        debug.field("bDeviceProtocol", &self.descriptor.bDeviceProtocol);
+        debug.field("bMaxPacketSize", &self.descriptor.bMaxPacketSize0);
+        debug.field("idVendor", &self.descriptor.idVendor);
+        debug.field("idProduct", &self.descriptor.idProduct);
+        debug.field("bcdDevice", &self.descriptor.bcdDevice);
+        debug.field("iManufacturer", &self.descriptor.iManufacturer);
+        debug.field("iProduct", &self.descriptor.iProduct);
+        debug.field("iSerialNumber", &self.descriptor.iSerialNumber);
+        debug.field("bNumConfigurations", &self.descriptor.bNumConfigurations);
+
+        debug.finish()
+    }
+}
+
+#[doc(hidden)]
+pub fn from_libusb(device: libusb_device_descriptor) -> DeviceDescriptor {
+    DeviceDescriptor { descriptor: device }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::fields::Version;
+
+    #[test]
+    fn it_has_usb_version() {
+        assert_eq!(
+            Version::from_bcd(0x1234),
+            super::from_libusb(device_descriptor!(bcdUSB: 0x1234)).usb_version()
+        );
+    }
+
+    #[test]
+    fn it_has_device_version() {
+        assert_eq!(
+            Version::from_bcd(0x1234),
+            super::from_libusb(device_descriptor!(bcdDevice: 0x1234)).device_version()
+        );
+    }
+
+    #[test]
+    fn it_has_manufacturer_string_index() {
+        assert_eq!(
+            Some(42),
+            super::from_libusb(device_descriptor!(iManufacturer: 42)).manufacturer_string_index()
+        );
+    }
+
+    #[test]
+    fn it_handles_missing_manufacturer_string_index() {
+        assert_eq!(
+            None,
+            super::from_libusb(device_descriptor!(iManufacturer: 0)).manufacturer_string_index()
+        );
+    }
+
+    #[test]
+    fn it_has_product_string_index() {
+        assert_eq!(
+            Some(42),
+            super::from_libusb(device_descriptor!(iProduct: 42)).product_string_index()
+        );
+    }
+
+    #[test]
+    fn it_handles_missing_product_string_index() {
+        assert_eq!(
+            None,
+            super::from_libusb(device_descriptor!(iProduct: 0)).product_string_index()
+        );
+    }
+
+    #[test]
+    fn it_has_serial_number_string_index() {
+        assert_eq!(
+            Some(42),
+            super::from_libusb(device_descriptor!(iSerialNumber: 42)).serial_number_string_index()
+        );
+    }
+
+    #[test]
+    fn it_handles_missing_serial_number_string_index() {
+        assert_eq!(
+            None,
+            super::from_libusb(device_descriptor!(iSerialNumber: 0)).serial_number_string_index()
+        );
+    }
+
+    #[test]
+    fn it_has_class_code() {
+        assert_eq!(
+            42,
+            super::from_libusb(device_descriptor!(bDeviceClass: 42)).class_code()
+        );
+    }
+
+    #[test]
+    fn it_has_sub_class_code() {
+        assert_eq!(
+            42,
+            super::from_libusb(device_descriptor!(bDeviceSubClass: 42)).sub_class_code()
+        );
+    }
+
+    #[test]
+    fn it_has_protocol_code() {
+        assert_eq!(
+            42,
+            super::from_libusb(device_descriptor!(bDeviceProtocol: 42)).protocol_code()
+        );
+    }
+
+    #[test]
+    fn it_has_vendor_id() {
+        assert_eq!(
+            42,
+            super::from_libusb(device_descriptor!(idVendor: 42)).vendor_id()
+        );
+    }
+
+    #[test]
+    fn it_has_product_id() {
+        assert_eq!(
+            42,
+            super::from_libusb(device_descriptor!(idProduct: 42)).product_id()
+        );
+    }
+
+    #[test]
+    fn it_has_max_packet_size() {
+        assert_eq!(
+            42,
+            super::from_libusb(device_descriptor!(bMaxPacketSize0: 42)).max_packet_size()
+        );
+    }
+
+    #[test]
+    fn it_has_num_configurations() {
+        assert_eq!(
+            3,
+            super::from_libusb(device_descriptor!(bNumConfigurations: 3)).num_configurations()
+        );
+    }
+}
diff --git a/src/device_handle.rs b/src/device_handle.rs
new file mode 100644
index 0000000..3bf19ba
--- /dev/null
+++ b/src/device_handle.rs
@@ -0,0 +1,939 @@
+use std::{
+    fmt::{self, Debug},
+    mem,
+    ptr::NonNull,
+    sync::Mutex,
+    time::Duration,
+};
+
+use libc::{c_int, c_uchar, c_uint};
+use libusb1_sys::{constants::*, *};
+
+use crate::{
+    config_descriptor::ConfigDescriptor,
+    device::{self, Device},
+    device_descriptor::DeviceDescriptor,
+    error::{self, Error},
+    fields::{request_type, Direction, Recipient, RequestType},
+    interface_descriptor::InterfaceDescriptor,
+    language::Language,
+    UsbContext,
+};
+
+/// Bit set representing claimed USB interfaces.
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+struct ClaimedInterfaces {
+    inner: [u128; 2],
+}
+
+impl ClaimedInterfaces {
+    /// Create a new bit set.
+    fn new() -> Self {
+        Self { inner: [0, 0] }
+    }
+
+    fn get_index_and_mask(interface: u8) -> (usize, u128) {
+        ((interface / 128) as usize, 1 << (interface % 128))
+    }
+
+    /// Mark `interface` as claimed.
+    fn insert(&mut self, interface: u8) {
+        let (index, mask) = ClaimedInterfaces::get_index_and_mask(interface);
+        self.inner[index] |= mask;
+    }
+
+    /// Mark `interface` as not claimed.
+    fn remove(&mut self, interface: u8) {
+        let (index, mask) = ClaimedInterfaces::get_index_and_mask(interface);
+        self.inner[index] &= !mask;
+    }
+
+    /// Returns true if this set contains `interface`.
+    fn contains(&self, interface: u8) -> bool {
+        let (index, mask) = ClaimedInterfaces::get_index_and_mask(interface);
+        self.inner[index] & mask != 0
+    }
+
+    /// Returns a count of the interfaces contained in this set.
+    fn size(&self) -> usize {
+        self.inner.iter().map(|v| v.count_ones()).sum::<u32>() as usize
+    }
+
+    /// Returns an iterator over the interfaces in this set.
+    fn iter(&self) -> ClaimedInterfacesIter {
+        ClaimedInterfacesIter::new(self)
+    }
+}
+
+/// Iterator over interfaces.
+struct ClaimedInterfacesIter<'a> {
+    // Next interface to check as a possible value to return from the interator.
+    index: u16,
+
+    // Number of elements remaining in this iterator.
+    remaining: usize,
+
+    // The ClaimedInterfaces object that we're iterating over.
+    source: &'a ClaimedInterfaces,
+}
+
+impl ClaimedInterfacesIter<'_> {
+    /// Create a new iterator over the interfaces in `source`.
+    fn new(source: &ClaimedInterfaces) -> ClaimedInterfacesIter {
+        ClaimedInterfacesIter {
+            index: 0,
+            remaining: source.size(),
+            source,
+        }
+    }
+}
+
+impl<'a> Iterator for ClaimedInterfacesIter<'a> {
+    type Item = u8;
+
+    fn next(&mut self) -> Option<u8> {
+        while self.index <= u8::MAX as u16 {
+            let index = self.index as u8;
+            let contains = self.source.contains(index);
+            self.index += 1;
+            if contains {
+                self.remaining -= 1;
+                return Some(index);
+            }
+        }
+        None
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.remaining, Some(self.remaining))
+    }
+}
+
+/// A handle to an open USB device.
+pub struct DeviceHandle<T: UsbContext> {
+    context: T,
+    handle: Option<NonNull<libusb_device_handle>>,
+    interfaces: Mutex<ClaimedInterfaces>,
+}
+
+impl<T: UsbContext> Drop for DeviceHandle<T> {
+    /// Closes the device.
+    fn drop(&mut self) {
+        unsafe {
+            let interfaces = self.interfaces.lock().unwrap();
+            for iface in interfaces.iter() {
+                libusb_release_interface(self.as_raw(), iface as c_int);
+            }
+
+            if let Some(handle) = self.handle {
+                libusb_close(handle.as_ptr());
+            }
+        }
+    }
+}
+
+unsafe impl<T: UsbContext> Send for DeviceHandle<T> {}
+unsafe impl<T: UsbContext> Sync for DeviceHandle<T> {}
+
+impl<T: UsbContext> Debug for DeviceHandle<T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("DeviceHandle")
+            .field("device", &self.device())
+            .field("handle", &self.handle)
+            .field("interfaces", &*self.interfaces.lock().unwrap())
+            .finish()
+    }
+}
+
+impl<T: UsbContext + PartialEq> PartialEq for DeviceHandle<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.context == other.context
+            && self.handle == other.handle
+            && *self.interfaces.lock().unwrap() == *other.interfaces.lock().unwrap()
+    }
+}
+
+impl<T: UsbContext + PartialEq> Eq for DeviceHandle<T> {}
+
+impl<T: UsbContext> DeviceHandle<T> {
+    /// Get the raw libusb_device_handle pointer, for advanced use in unsafe code.
+    ///
+    /// This structure tracks claimed interfaces, and will get out if sync if interfaces are
+    /// manipulated externally. Use only libusb endpoint IO functions.
+    pub fn as_raw(&self) -> *mut libusb_device_handle {
+        // Safety: handle is Some() after initialization
+        match self.handle {
+            Some(it) => it.as_ptr(),
+            _ => unreachable!(),
+        }
+    }
+
+    /// Consumes the `DeviceHandle`, returning the raw libusb_device_handle
+    /// pointer, for advanced use in unsafe code.
+    ///
+    /// # Safety
+    ///
+    /// Panics if you have any claimed interfaces on this handle.
+    pub fn into_raw(mut self) -> *mut libusb_device_handle {
+        assert_eq!(self.interfaces.lock().unwrap().size(), 0);
+        match self.handle.take() {
+            Some(it) => it.as_ptr(),
+            _ => unreachable!(),
+        }
+    }
+
+    /// Get the context associated with this device
+    pub fn context(&self) -> &T {
+        &self.context
+    }
+
+    /// Get the device associated to this handle
+    pub fn device(&self) -> Device<T> {
+        unsafe {
+            device::Device::from_libusb(
+                self.context.clone(),
+                std::ptr::NonNull::new_unchecked(libusb_get_device(self.as_raw())),
+            )
+        }
+    }
+
+    /// # Safety
+    ///
+    /// Converts an existing `libusb_device_handle` pointer into a `DeviceHandle<T>`.
+    /// `handle` must be a pointer to a valid `libusb_device_handle`. Rusb assumes ownership of the handle, and will close it on `drop`.
+    pub unsafe fn from_libusb(
+        context: T,
+        handle: NonNull<libusb_device_handle>,
+    ) -> DeviceHandle<T> {
+        DeviceHandle {
+            context,
+            handle: Some(handle),
+            interfaces: Mutex::new(ClaimedInterfaces::new()),
+        }
+    }
+
+    /// Returns the active configuration number.
+    pub fn active_configuration(&self) -> crate::Result<u8> {
+        let mut config = mem::MaybeUninit::<c_int>::uninit();
+
+        try_unsafe!(libusb_get_configuration(self.as_raw(), config.as_mut_ptr()));
+        Ok(unsafe { config.assume_init() } as u8)
+    }
+
+    /// Sets the device's active configuration.
+    pub fn set_active_configuration(&self, config: u8) -> crate::Result<()> {
+        try_unsafe!(libusb_set_configuration(self.as_raw(), c_int::from(config)));
+        Ok(())
+    }
+
+    /// Puts the device in an unconfigured state.
+    pub fn unconfigure(&self) -> crate::Result<()> {
+        try_unsafe!(libusb_set_configuration(self.as_raw(), -1));
+        Ok(())
+    }
+
+    /// Resets the device.
+    pub fn reset(&self) -> crate::Result<()> {
+        try_unsafe!(libusb_reset_device(self.as_raw()));
+        Ok(())
+    }
+
+    /// Clear the halt/stall condition for an endpoint.
+    pub fn clear_halt(&self, endpoint: u8) -> crate::Result<()> {
+        try_unsafe!(libusb_clear_halt(self.as_raw(), endpoint));
+        Ok(())
+    }
+
+    /// Indicates whether the device has an attached kernel driver.
+    ///
+    /// This method is not supported on all platforms.
+    pub fn kernel_driver_active(&self, iface: u8) -> crate::Result<bool> {
+        match unsafe { libusb_kernel_driver_active(self.as_raw(), c_int::from(iface)) } {
+            0 => Ok(false),
+            1 => Ok(true),
+            err => Err(error::from_libusb(err)),
+        }
+    }
+
+    /// Detaches an attached kernel driver from the device.
+    ///
+    /// This method is not supported on all platforms.
+    pub fn detach_kernel_driver(&self, iface: u8) -> crate::Result<()> {
+        try_unsafe!(libusb_detach_kernel_driver(
+            self.as_raw(),
+            c_int::from(iface)
+        ));
+        Ok(())
+    }
+
+    /// Attaches a kernel driver to the device.
+    ///
+    /// This method is not supported on all platforms.
+    pub fn attach_kernel_driver(&self, iface: u8) -> crate::Result<()> {
+        try_unsafe!(libusb_attach_kernel_driver(
+            self.as_raw(),
+            c_int::from(iface)
+        ));
+        Ok(())
+    }
+
+    /// Enable/disable automatic kernel driver detachment.
+    ///
+    /// When this is enabled rusb will automatically detach the
+    /// kernel driver on an interface when claiming the interface, and
+    /// attach it when releasing the interface.
+    ///
+    /// On platforms which do not have support, this function will
+    /// return `Error::NotSupported`, and rusb will continue as if
+    /// this function was never called.
+    pub fn set_auto_detach_kernel_driver(&self, auto_detach: bool) -> crate::Result<()> {
+        try_unsafe!(libusb_set_auto_detach_kernel_driver(
+            self.as_raw(),
+            auto_detach.into()
+        ));
+        Ok(())
+    }
+
+    /// Claims one of the device's interfaces.
+    ///
+    /// An interface must be claimed before operating on it. All claimed interfaces are released
+    /// when the device handle goes out of scope.
+    pub fn claim_interface(&self, iface: u8) -> crate::Result<()> {
+        try_unsafe!(libusb_claim_interface(self.as_raw(), c_int::from(iface)));
+        self.interfaces.lock().unwrap().insert(iface);
+        Ok(())
+    }
+
+    /// Releases a claimed interface.
+    pub fn release_interface(&self, iface: u8) -> crate::Result<()> {
+        try_unsafe!(libusb_release_interface(self.as_raw(), c_int::from(iface)));
+        self.interfaces.lock().unwrap().remove(iface);
+        Ok(())
+    }
+
+    /// Sets an interface's active setting.
+    pub fn set_alternate_setting(&self, iface: u8, setting: u8) -> crate::Result<()> {
+        try_unsafe!(libusb_set_interface_alt_setting(
+            self.as_raw(),
+            c_int::from(iface),
+            c_int::from(setting)
+        ));
+        Ok(())
+    }
+
+    /// Reads from an interrupt endpoint.
+    ///
+    /// This function attempts to read from the interrupt endpoint with the address given by the
+    /// `endpoint` parameter and fills `buf` with any data received from the endpoint. The function
+    /// blocks up to the amount of time specified by `timeout`. Minimal `timeout` is 1 milliseconds,
+    /// anything smaller will result in an infinite block.
+    ///
+    /// If the return value is `Ok(n)`, then `buf` is populated with `n` bytes of data received
+    /// from the endpoint.
+    ///
+    /// ## Errors
+    ///
+    /// If this function encounters any form of error while fulfilling the transfer request, an
+    /// error variant will be returned. If an error variant is returned, no bytes were read.
+    ///
+    /// The errors returned by this function include:
+    ///
+    /// * `InvalidParam` if the endpoint is not an input endpoint.
+    /// * `Timeout` if the transfer timed out.
+    /// * `Pipe` if the endpoint halted.
+    /// * `Overflow` if the device offered more data.
+    /// * `NoDevice` if the device has been disconnected.
+    /// * `Io` if the transfer encountered an I/O error.
+    pub fn read_interrupt(
+        &self,
+        endpoint: u8,
+        buf: &mut [u8],
+        timeout: Duration,
+    ) -> crate::Result<usize> {
+        if endpoint & LIBUSB_ENDPOINT_DIR_MASK != LIBUSB_ENDPOINT_IN {
+            return Err(Error::InvalidParam);
+        }
+        let mut transferred = mem::MaybeUninit::<c_int>::uninit();
+        unsafe {
+            match libusb_interrupt_transfer(
+                self.as_raw(),
+                endpoint,
+                buf.as_mut_ptr() as *mut c_uchar,
+                buf.len() as c_int,
+                transferred.as_mut_ptr(),
+                timeout.as_millis() as c_uint,
+            ) {
+                0 => Ok(transferred.assume_init() as usize),
+                err if err == LIBUSB_ERROR_INTERRUPTED => {
+                    let transferred = transferred.assume_init();
+                    if transferred > 0 {
+                        Ok(transferred as usize)
+                    } else {
+                        Err(error::from_libusb(err))
+                    }
+                }
+                err => Err(error::from_libusb(err)),
+            }
+        }
+    }
+
+    /// Writes to an interrupt endpoint.
+    ///
+    /// This function attempts to write the contents of `buf` to the interrupt endpoint with the
+    /// address given by the `endpoint` parameter. The function blocks up to the amount of time
+    /// specified by `timeout`. Minimal `timeout` is 1 milliseconds, anything smaller will
+    /// result in an infinite block.
+    ///
+    /// If the return value is `Ok(n)`, then `n` bytes of `buf` were written to the endpoint.
+    ///
+    /// ## Errors
+    ///
+    /// If this function encounters any form of error while fulfilling the transfer request, an
+    /// error variant will be returned. If an error variant is returned, no bytes were written.
+    ///
+    /// The errors returned by this function include:
+    ///
+    /// * `InvalidParam` if the endpoint is not an output endpoint.
+    /// * `Timeout` if the transfer timed out.
+    /// * `Pipe` if the endpoint halted.
+    /// * `NoDevice` if the device has been disconnected.
+    /// * `Io` if the transfer encountered an I/O error.
+    pub fn write_interrupt(
+        &self,
+        endpoint: u8,
+        buf: &[u8],
+        timeout: Duration,
+    ) -> crate::Result<usize> {
+        if endpoint & LIBUSB_ENDPOINT_DIR_MASK != LIBUSB_ENDPOINT_OUT {
+            return Err(Error::InvalidParam);
+        }
+        let mut transferred = mem::MaybeUninit::<c_int>::uninit();
+        unsafe {
+            match libusb_interrupt_transfer(
+                self.as_raw(),
+                endpoint,
+                buf.as_ptr() as *mut c_uchar,
+                buf.len() as c_int,
+                transferred.as_mut_ptr(),
+                timeout.as_millis() as c_uint,
+            ) {
+                0 => Ok(transferred.assume_init() as usize),
+                err if err == LIBUSB_ERROR_INTERRUPTED => {
+                    let transferred = transferred.assume_init();
+                    if transferred > 0 {
+                        Ok(transferred as usize)
+                    } else {
+                        Err(error::from_libusb(err))
+                    }
+                }
+                err => Err(error::from_libusb(err)),
+            }
+        }
+    }
+
+    /// Reads from a bulk endpoint.
+    ///
+    /// This function attempts to read from the bulk endpoint with the address given by the
+    /// `endpoint` parameter and fills `buf` with any data received from the endpoint. The function
+    /// blocks up to the amount of time specified by `timeout`. Minimal `timeout` is 1 milliseconds,
+    /// anything smaller will result in an infinite block.
+    ///
+    /// If the return value is `Ok(n)`, then `buf` is populated with `n` bytes of data received
+    /// from the endpoint.
+    ///
+    /// ## Errors
+    ///
+    /// If this function encounters any form of error while fulfilling the transfer request, an
+    /// error variant will be returned. If an error variant is returned, no bytes were read.
+    ///
+    /// The errors returned by this function include:
+    ///
+    /// * `InvalidParam` if the endpoint is not an input endpoint.
+    /// * `Timeout` if the transfer timed out.
+    /// * `Pipe` if the endpoint halted.
+    /// * `Overflow` if the device offered more data.
+    /// * `NoDevice` if the device has been disconnected.
+    /// * `Io` if the transfer encountered an I/O error.
+    pub fn read_bulk(
+        &self,
+        endpoint: u8,
+        buf: &mut [u8],
+        timeout: Duration,
+    ) -> crate::Result<usize> {
+        if endpoint & LIBUSB_ENDPOINT_DIR_MASK != LIBUSB_ENDPOINT_IN {
+            return Err(Error::InvalidParam);
+        }
+        let mut transferred = mem::MaybeUninit::<c_int>::uninit();
+        unsafe {
+            match libusb_bulk_transfer(
+                self.as_raw(),
+                endpoint,
+                buf.as_mut_ptr() as *mut c_uchar,
+                buf.len() as c_int,
+                transferred.as_mut_ptr(),
+                timeout.as_millis() as c_uint,
+            ) {
+                0 => Ok(transferred.assume_init() as usize),
+                err if err == LIBUSB_ERROR_INTERRUPTED || err == LIBUSB_ERROR_TIMEOUT => {
+                    let transferred = transferred.assume_init();
+                    if transferred > 0 {
+                        Ok(transferred as usize)
+                    } else {
+                        Err(error::from_libusb(err))
+                    }
+                }
+                err => Err(error::from_libusb(err)),
+            }
+        }
+    }
+
+    /// Writes to a bulk endpoint.
+    ///
+    /// This function attempts to write the contents of `buf` to the bulk endpoint with the address
+    /// given by the `endpoint` parameter. The function blocks up to the amount of time specified
+    /// by `timeout`. Minimal `timeout` is 1 milliseconds, anything smaller will result in an
+    /// infinite block.
+    ///
+    /// If the return value is `Ok(n)`, then `n` bytes of `buf` were written to the endpoint.
+    ///
+    /// ## Errors
+    ///
+    /// If this function encounters any form of error while fulfilling the transfer request, an
+    /// error variant will be returned. If an error variant is returned, no bytes were written.
+    ///
+    /// The errors returned by this function include:
+    ///
+    /// * `InvalidParam` if the endpoint is not an output endpoint.
+    /// * `Timeout` if the transfer timed out.
+    /// * `Pipe` if the endpoint halted.
+    /// * `NoDevice` if the device has been disconnected.
+    /// * `Io` if the transfer encountered an I/O error.
+    pub fn write_bulk(&self, endpoint: u8, buf: &[u8], timeout: Duration) -> crate::Result<usize> {
+        if endpoint & LIBUSB_ENDPOINT_DIR_MASK != LIBUSB_ENDPOINT_OUT {
+            return Err(Error::InvalidParam);
+        }
+        let mut transferred = mem::MaybeUninit::<c_int>::uninit();
+        unsafe {
+            match libusb_bulk_transfer(
+                self.as_raw(),
+                endpoint,
+                buf.as_ptr() as *mut c_uchar,
+                buf.len() as c_int,
+                transferred.as_mut_ptr(),
+                timeout.as_millis() as c_uint,
+            ) {
+                0 => Ok(transferred.assume_init() as usize),
+                err if err == LIBUSB_ERROR_INTERRUPTED || err == LIBUSB_ERROR_TIMEOUT => {
+                    let transferred = transferred.assume_init();
+                    if transferred > 0 {
+                        Ok(transferred as usize)
+                    } else {
+                        Err(error::from_libusb(err))
+                    }
+                }
+                err => Err(error::from_libusb(err)),
+            }
+        }
+    }
+
+    /// Reads data using a control transfer.
+    ///
+    /// This function attempts to read data from the device using a control transfer and fills
+    /// `buf` with any data received during the transfer. The function blocks up to the amount of
+    /// time specified by `timeout`. Minimal `timeout` is 1 milliseconds, anything smaller will
+    /// result in an infinite block.
+    ///
+    /// The parameters `request_type`, `request`, `value`, and `index` specify the fields of the
+    /// control transfer setup packet (`bmRequestType`, `bRequest`, `wValue`, and `wIndex`
+    /// respectively). The values for each of these parameters shall be given in host-endian byte
+    /// order. The value for the `request_type` parameter can be built with the helper function,
+    /// [request_type()](fn.request_type.html). The meaning of the other parameters depends on the
+    /// type of control request.
+    ///
+    /// If the return value is `Ok(n)`, then `buf` is populated with `n` bytes of data.
+    ///
+    /// ## Errors
+    ///
+    /// If this function encounters any form of error while fulfilling the transfer request, an
+    /// error variant will be returned. If an error variant is returned, no bytes were read.
+    ///
+    /// The errors returned by this function include:
+    ///
+    /// * `InvalidParam` if `request_type` does not specify a read transfer.
+    /// * `Timeout` if the transfer timed out.
+    /// * `Pipe` if the control request was not supported by the device.
+    /// * `NoDevice` if the device has been disconnected.
+    /// * `Io` if the transfer encountered an I/O error.
+    pub fn read_control(
+        &self,
+        request_type: u8,
+        request: u8,
+        value: u16,
+        index: u16,
+        buf: &mut [u8],
+        timeout: Duration,
+    ) -> crate::Result<usize> {
+        if request_type & LIBUSB_ENDPOINT_DIR_MASK != LIBUSB_ENDPOINT_IN {
+            return Err(Error::InvalidParam);
+        }
+        let res = unsafe {
+            libusb_control_transfer(
+                self.as_raw(),
+                request_type,
+                request,
+                value,
+                index,
+                buf.as_mut_ptr() as *mut c_uchar,
+                buf.len() as u16,
+                timeout.as_millis() as c_uint,
+            )
+        };
+
+        if res < 0 {
+            Err(error::from_libusb(res))
+        } else {
+            Ok(res as usize)
+        }
+    }
+
+    /// Writes data using a control transfer.
+    ///
+    /// This function attempts to write the contents of `buf` to the device using a control
+    /// transfer. The function blocks up to the amount of time specified by `timeout`.
+    /// Minimal `timeout` is 1 milliseconds, anything smaller will result in an infinite block.
+    ///
+    /// The parameters `request_type`, `request`, `value`, and `index` specify the fields of the
+    /// control transfer setup packet (`bmRequestType`, `bRequest`, `wValue`, and `wIndex`
+    /// respectively). The values for each of these parameters shall be given in host-endian byte
+    /// order. The value for the `request_type` parameter can be built with the helper function,
+    /// [request_type()](fn.request_type.html). The meaning of the other parameters depends on the
+    /// type of control request.
+    ///
+    /// If the return value is `Ok(n)`, then `n` bytes of `buf` were transfered.
+    ///
+    /// ## Errors
+    ///
+    /// If this function encounters any form of error while fulfilling the transfer request, an
+    /// error variant will be returned. If an error variant is returned, no bytes were read.
+    ///
+    /// The errors returned by this function include:
+    ///
+    /// * `InvalidParam` if `request_type` does not specify a write transfer.
+    /// * `Timeout` if the transfer timed out.
+    /// * `Pipe` if the control request was not supported by the device.
+    /// * `NoDevice` if the device has been disconnected.
+    /// * `Io` if the transfer encountered an I/O error.
+    pub fn write_control(
+        &self,
+        request_type: u8,
+        request: u8,
+        value: u16,
+        index: u16,
+        buf: &[u8],
+        timeout: Duration,
+    ) -> crate::Result<usize> {
+        if request_type & LIBUSB_ENDPOINT_DIR_MASK != LIBUSB_ENDPOINT_OUT {
+            return Err(Error::InvalidParam);
+        }
+        let res = unsafe {
+            libusb_control_transfer(
+                self.as_raw(),
+                request_type,
+                request,
+                value,
+                index,
+                buf.as_ptr() as *mut c_uchar,
+                buf.len() as u16,
+                timeout.as_millis() as c_uint,
+            )
+        };
+
+        if res < 0 {
+            Err(error::from_libusb(res))
+        } else {
+            Ok(res as usize)
+        }
+    }
+
+    /// Reads the languages supported by the device's string descriptors.
+    ///
+    /// This function returns a list of languages that can be used to read the device's string
+    /// descriptors.
+    pub fn read_languages(&self, timeout: Duration) -> crate::Result<Vec<Language>> {
+        let mut buf = [0u8; 255];
+
+        let len = self.read_control(
+            request_type(Direction::In, RequestType::Standard, Recipient::Device),
+            LIBUSB_REQUEST_GET_DESCRIPTOR,
+            u16::from(LIBUSB_DT_STRING) << 8,
+            0,
+            &mut buf,
+            timeout,
+        )?;
+
+        if len < 2 || buf[0] != len as u8 || len & 0x01 != 0 {
+            return Err(Error::BadDescriptor);
+        }
+
+        if len == 2 {
+            return Ok(Vec::new());
+        }
+
+        Ok(buf[0..len]
+            .chunks(2)
+            .skip(1)
+            .map(|chunk| {
+                let lang_id = u16::from(chunk[0]) | u16::from(chunk[1]) << 8;
+                crate::language::from_lang_id(lang_id)
+            })
+            .collect())
+    }
+
+    /// Reads a ascii string descriptor from the device.
+    ///
+    pub fn read_string_descriptor_ascii(&self, index: u8) -> crate::Result<String> {
+        let mut buf = Vec::<u8>::with_capacity(255);
+
+        let ptr = buf.as_mut_ptr() as *mut c_uchar;
+        let capacity = buf.capacity() as i32;
+
+        let res =
+            unsafe { libusb_get_string_descriptor_ascii(self.as_raw(), index, ptr, capacity) };
+
+        if res < 0 {
+            return Err(error::from_libusb(res));
+        }
+
+        unsafe {
+            buf.set_len(res as usize);
+        }
+
+        String::from_utf8(buf).map_err(|_| Error::Other)
+    }
+
+    /// Reads a string descriptor from the device.
+    ///
+    /// `language` should be one of the languages returned from [`read_languages`](#method.read_languages).
+    pub fn read_string_descriptor(
+        &self,
+        language: Language,
+        index: u8,
+        timeout: Duration,
+    ) -> crate::Result<String> {
+        let mut buf = [0u16; 128];
+
+        let len = {
+            // SAFETY: since we create slice from existing slice pointer valid
+            // alignment of [u8] less or equal to the [u16]
+            // size is less then allocated buffer (128 * 2 = 256 => 256 < 255)
+            let buf = unsafe {
+                std::slice::from_raw_parts_mut(
+                    buf.as_mut_ptr().cast::<u8>(),
+                    255, // Some devices choke on size > 255
+                )
+            };
+
+            let len = self.read_control(
+                request_type(Direction::In, RequestType::Standard, Recipient::Device),
+                LIBUSB_REQUEST_GET_DESCRIPTOR,
+                u16::from(LIBUSB_DT_STRING) << 8 | u16::from(index),
+                language.lang_id(),
+                buf,
+                timeout,
+            )?;
+
+            if len < 2 || buf[0] != len as u8 || len & 0x01 != 0 {
+                return Err(Error::BadDescriptor);
+            }
+
+            len
+        };
+
+        if len == 2 {
+            return Ok(String::new());
+        }
+
+        // len in bytes, skip first element(it's contain descriptor type and len)
+        String::from_utf16(&buf[1..(len / 2)]).map_err(|_| Error::Other)
+    }
+
+    /// Reads the device's manufacturer string descriptor (ascii).
+    pub fn read_manufacturer_string_ascii(
+        &self,
+        device: &DeviceDescriptor,
+    ) -> crate::Result<String> {
+        match device.manufacturer_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor_ascii(n),
+        }
+    }
+
+    /// Reads the device's manufacturer string descriptor.
+    pub fn read_manufacturer_string(
+        &self,
+        language: Language,
+        device: &DeviceDescriptor,
+        timeout: Duration,
+    ) -> crate::Result<String> {
+        match device.manufacturer_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor(language, n, timeout),
+        }
+    }
+
+    /// Reads the device's product string descriptor (ascii).
+    pub fn read_product_string_ascii(&self, device: &DeviceDescriptor) -> crate::Result<String> {
+        match device.product_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor_ascii(n),
+        }
+    }
+
+    /// Reads the device's product string descriptor.
+    pub fn read_product_string(
+        &self,
+        language: Language,
+        device: &DeviceDescriptor,
+        timeout: Duration,
+    ) -> crate::Result<String> {
+        match device.product_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor(language, n, timeout),
+        }
+    }
+
+    /// Reads the device's serial number string descriptor (ascii).
+    pub fn read_serial_number_string_ascii(
+        &self,
+        device: &DeviceDescriptor,
+    ) -> crate::Result<String> {
+        match device.serial_number_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor_ascii(n),
+        }
+    }
+
+    /// Reads the device's serial number string descriptor.
+    pub fn read_serial_number_string(
+        &self,
+        language: Language,
+        device: &DeviceDescriptor,
+        timeout: Duration,
+    ) -> crate::Result<String> {
+        match device.serial_number_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor(language, n, timeout),
+        }
+    }
+
+    /// Reads the string descriptor for a configuration's description.
+    pub fn read_configuration_string(
+        &self,
+        language: Language,
+        configuration: &ConfigDescriptor,
+        timeout: Duration,
+    ) -> crate::Result<String> {
+        match configuration.description_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor(language, n, timeout),
+        }
+    }
+
+    /// Reads the string descriptor for a interface's description.
+    pub fn read_interface_string(
+        &self,
+        language: Language,
+        interface: &InterfaceDescriptor,
+        timeout: Duration,
+    ) -> crate::Result<String> {
+        match interface.description_string_index() {
+            None => Err(Error::InvalidParam),
+            Some(n) => self.read_string_descriptor(language, n, timeout),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::ClaimedInterfaces;
+    use std::u8;
+
+    #[test]
+    fn claimed_interfaces_empty() {
+        let empty = ClaimedInterfaces::new();
+        assert_eq!(empty.size(), 0);
+        for i in 0..=u8::MAX {
+            assert!(!empty.contains(i), "empty set should not contain {}", i);
+        }
+
+        let mut iter = empty.iter();
+        assert_eq!(iter.size_hint(), (0, Some(0)));
+        assert_eq!(iter.next(), None);
+    }
+
+    #[test]
+    fn claimed_interfaces_one_element() {
+        let mut interfaces = ClaimedInterfaces::new();
+        interfaces.insert(94);
+        assert_eq!(interfaces.size(), 1);
+        assert!(interfaces.contains(94));
+        for i in 0..=u8::MAX {
+            if i == 94 {
+                continue;
+            }
+            assert!(
+                !interfaces.contains(i),
+                "interfaces should not contain {}",
+                i
+            );
+        }
+
+        let mut iter = interfaces.iter();
+        assert_eq!(iter.size_hint(), (1, Some(1)));
+        assert_eq!(iter.next(), Some(94));
+        assert_eq!(iter.size_hint(), (0, Some(0)));
+        assert_eq!(iter.next(), None);
+    }
+
+    #[test]
+    fn claimed_interfaces_many_elements() {
+        let mut interfaces = ClaimedInterfaces::new();
+        let elements = vec![94, 0, 255, 17, 183, 6];
+
+        for (index, &interface) in elements.iter().enumerate() {
+            interfaces.insert(interface);
+            assert_eq!(interfaces.size(), index + 1);
+        }
+
+        // Validate contains().
+        for &interface in elements.iter() {
+            assert!(
+                interfaces.contains(interface),
+                "interfaces should contain {}",
+                interface
+            );
+        }
+
+        // Validate iter().
+        let contents = interfaces.iter().collect::<Vec<_>>().sort();
+        assert_eq!(contents, elements.clone().sort());
+
+        // Validate size_hint().
+        let mut iter = interfaces.iter();
+        let mut read = 0;
+        loop {
+            assert!(
+                read <= elements.len(),
+                "read elements {} should not exceed elements size {}",
+                read,
+                elements.len()
+            );
+            let remaining = elements.len() - read;
+            assert_eq!(iter.size_hint(), (remaining, Some(remaining)));
+            match iter.next() {
+                Some(_) => read += 1,
+                None => break,
+            }
+        }
+    }
+}
diff --git a/src/device_list.rs b/src/device_list.rs
new file mode 100644
index 0000000..3eaf757
--- /dev/null
+++ b/src/device_list.rs
@@ -0,0 +1,120 @@
+use libc::c_int;
+
+use std::{mem, slice};
+
+use crate::{
+    context::{GlobalContext, UsbContext},
+    device::{self, Device},
+    error,
+};
+use libusb1_sys::*;
+
+/// A list of detected USB devices.
+pub struct DeviceList<T: UsbContext> {
+    context: T,
+    list: *const *mut libusb_device,
+    len: usize,
+}
+
+impl<T: UsbContext> Drop for DeviceList<T> {
+    /// Frees the device list.
+    fn drop(&mut self) {
+        unsafe {
+            libusb_free_device_list(self.list, 1);
+        }
+    }
+}
+
+impl DeviceList<GlobalContext> {
+    pub fn new() -> crate::Result<DeviceList<GlobalContext>> {
+        let mut list = mem::MaybeUninit::<*const *mut libusb_device>::uninit();
+
+        let n =
+            unsafe { libusb_get_device_list(GlobalContext::default().as_raw(), list.as_mut_ptr()) };
+
+        if n < 0 {
+            Err(error::from_libusb(n as c_int))
+        } else {
+            Ok(unsafe {
+                DeviceList {
+                    context: Default::default(),
+                    list: list.assume_init(),
+                    len: n as usize,
+                }
+            })
+        }
+    }
+}
+
+impl<T: UsbContext> DeviceList<T> {
+    pub fn new_with_context(context: T) -> crate::Result<DeviceList<T>> {
+        let mut list = mem::MaybeUninit::<*const *mut libusb_device>::uninit();
+
+        let len = unsafe { libusb_get_device_list(context.as_raw(), list.as_mut_ptr()) };
+
+        if len < 0 {
+            Err(error::from_libusb(len as c_int))
+        } else {
+            Ok(unsafe {
+                DeviceList {
+                    context,
+                    list: list.assume_init(),
+                    len: len as usize,
+                }
+            })
+        }
+    }
+
+    /// Returns the number of devices in the list.
+    pub fn len(&self) -> usize {
+        self.len
+    }
+
+    /// Returns true if the list is empty, else returns false.
+    pub fn is_empty(&self) -> bool {
+        self.len == 0
+    }
+
+    /// Returns an iterator over the devices in the list.
+    ///
+    /// The iterator yields a sequence of `Device` objects.
+    pub fn iter(&self) -> Devices<T> {
+        Devices {
+            context: self.context.clone(),
+            devices: unsafe { slice::from_raw_parts(self.list, self.len) },
+            index: 0,
+        }
+    }
+}
+
+/// Iterator over detected USB devices.
+pub struct Devices<'a, T> {
+    context: T,
+    devices: &'a [*mut libusb_device],
+    index: usize,
+}
+
+impl<'a, T: UsbContext> Iterator for Devices<'a, T> {
+    type Item = Device<T>;
+
+    fn next(&mut self) -> Option<Device<T>> {
+        if self.index < self.devices.len() {
+            let device = self.devices[self.index];
+
+            self.index += 1;
+            Some(unsafe {
+                device::Device::from_libusb(
+                    self.context.clone(),
+                    std::ptr::NonNull::new_unchecked(device),
+                )
+            })
+        } else {
+            None
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        let remaining = self.devices.len() - self.index;
+        (remaining, Some(remaining))
+    }
+}
diff --git a/src/endpoint_descriptor.rs b/src/endpoint_descriptor.rs
new file mode 100644
index 0000000..cefa70e
--- /dev/null
+++ b/src/endpoint_descriptor.rs
@@ -0,0 +1,286 @@
+use std::{fmt, slice};
+
+use libusb1_sys::{constants::*, libusb_endpoint_descriptor};
+
+use crate::fields::{Direction, SyncType, TransferType, UsageType};
+
+/// Describes an endpoint.
+pub struct EndpointDescriptor<'a> {
+    descriptor: &'a libusb_endpoint_descriptor,
+}
+
+impl<'a> EndpointDescriptor<'a> {
+    /// Returns the size of the descriptor in bytes
+    pub fn length(&self) -> u8 {
+        self.descriptor.bLength
+    }
+
+    /// Returns the descriptor type
+    pub fn descriptor_type(&self) -> u8 {
+        self.descriptor.bDescriptorType
+    }
+
+    /// Returns the endpoint's address.
+    pub fn address(&self) -> u8 {
+        self.descriptor.bEndpointAddress
+    }
+
+    /// Returns the endpoint number.
+    pub fn number(&self) -> u8 {
+        self.descriptor.bEndpointAddress & 0x07
+    }
+
+    /// Returns the endpoint's direction.
+    pub fn direction(&self) -> Direction {
+        match self.descriptor.bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK {
+            LIBUSB_ENDPOINT_OUT => Direction::Out,
+            LIBUSB_ENDPOINT_IN | _ => Direction::In,
+        }
+    }
+
+    /// Returns the endpoint's transfer type.
+    pub fn transfer_type(&self) -> TransferType {
+        match self.descriptor.bmAttributes & LIBUSB_TRANSFER_TYPE_MASK {
+            LIBUSB_TRANSFER_TYPE_CONTROL => TransferType::Control,
+            LIBUSB_TRANSFER_TYPE_ISOCHRONOUS => TransferType::Isochronous,
+            LIBUSB_TRANSFER_TYPE_BULK => TransferType::Bulk,
+            LIBUSB_TRANSFER_TYPE_INTERRUPT | _ => TransferType::Interrupt,
+        }
+    }
+
+    /// Returns the endpoint's synchronisation mode.
+    ///
+    /// The return value of this method is only valid for isochronous endpoints.
+    pub fn sync_type(&self) -> SyncType {
+        match (self.descriptor.bmAttributes & LIBUSB_ISO_SYNC_TYPE_MASK) >> 2 {
+            LIBUSB_ISO_SYNC_TYPE_NONE => SyncType::NoSync,
+            LIBUSB_ISO_SYNC_TYPE_ASYNC => SyncType::Asynchronous,
+            LIBUSB_ISO_SYNC_TYPE_ADAPTIVE => SyncType::Adaptive,
+            LIBUSB_ISO_SYNC_TYPE_SYNC | _ => SyncType::Synchronous,
+        }
+    }
+
+    /// Returns the endpoint's usage type.
+    ///
+    /// The return value of this method is only valid for isochronous endpoints.
+    pub fn usage_type(&self) -> UsageType {
+        match (self.descriptor.bmAttributes & LIBUSB_ISO_USAGE_TYPE_MASK) >> 4 {
+            LIBUSB_ISO_USAGE_TYPE_DATA => UsageType::Data,
+            LIBUSB_ISO_USAGE_TYPE_FEEDBACK => UsageType::Feedback,
+            LIBUSB_ISO_USAGE_TYPE_IMPLICIT => UsageType::FeedbackData,
+            _ => UsageType::Reserved,
+        }
+    }
+
+    /// Returns the endpoint's maximum packet size.
+    pub fn max_packet_size(&self) -> u16 {
+        self.descriptor.wMaxPacketSize
+    }
+
+    /// Returns the endpoint's polling interval.
+    pub fn interval(&self) -> u8 {
+        self.descriptor.bInterval
+    }
+
+    /// Returns the unknown 'extra' bytes that libusb does not understand.
+    pub fn extra(&'a self) -> Option<&'a [u8]> {
+        unsafe {
+            match (*self.descriptor).extra_length {
+                len if len > 0 => Some(slice::from_raw_parts(
+                    (*self.descriptor).extra,
+                    len as usize,
+                )),
+                _ => None,
+            }
+        }
+    }
+
+    /// For audio devices only: return the rate at which synchronization feedback is provided.
+    pub fn refresh(&self) -> u8 {
+        self.descriptor.bRefresh
+    }
+
+    /// For audio devices only: return the address if the synch endpoint.
+    pub fn synch_address(&self) -> u8 {
+        self.descriptor.bSynchAddress
+    }
+}
+
+impl<'a> fmt::Debug for EndpointDescriptor<'a> {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        let mut debug = fmt.debug_struct("EndpointDescriptor");
+
+        debug.field("bLength", &self.descriptor.bLength);
+        debug.field("bDescriptorType", &self.descriptor.bDescriptorType);
+        debug.field("bEndpointAddress", &self.descriptor.bEndpointAddress);
+        debug.field("bmAttributes", &self.descriptor.bmAttributes);
+        debug.field("wMaxPacketSize", &self.descriptor.wMaxPacketSize);
+        debug.field("bInterval", &self.descriptor.bInterval);
+
+        debug.finish()
+    }
+}
+
+#[doc(hidden)]
+pub(crate) fn from_libusb(endpoint: &libusb_endpoint_descriptor) -> EndpointDescriptor {
+    EndpointDescriptor {
+        descriptor: endpoint,
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::fields::{Direction, SyncType, TransferType, UsageType};
+
+    #[test]
+    fn it_interprets_number_for_output_endpoints() {
+        assert_eq!(
+            0,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b0000_0000)).number()
+        );
+        assert_eq!(
+            1,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b0000_0001)).number()
+        );
+    }
+
+    #[test]
+    fn it_interprets_number_for_input_endpoints() {
+        assert_eq!(
+            2,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b1000_0010)).number()
+        );
+        assert_eq!(
+            3,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b1000_0011)).number()
+        );
+    }
+
+    #[test]
+    fn it_ignores_reserved_bits_in_address() {
+        assert_eq!(
+            0,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b0000_1000)).number()
+        );
+        assert_eq!(
+            0,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b0001_0000)).number()
+        );
+        assert_eq!(
+            0,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b0010_0000)).number()
+        );
+        assert_eq!(
+            0,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b0100_0000)).number()
+        );
+        assert_eq!(
+            7,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b1111_1111)).number()
+        );
+    }
+
+    #[test]
+    fn it_interprets_direction_bit_in_address() {
+        assert_eq!(
+            Direction::Out,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b0000_0000)).direction()
+        );
+        assert_eq!(
+            Direction::In,
+            super::from_libusb(&endpoint_descriptor!(bEndpointAddress: 0b1000_0000)).direction()
+        );
+    }
+
+    #[test]
+    fn it_interprets_transfer_type_in_attributes() {
+        assert_eq!(
+            TransferType::Control,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_0000)).transfer_type()
+        );
+        assert_eq!(
+            TransferType::Isochronous,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_0001)).transfer_type()
+        );
+        assert_eq!(
+            TransferType::Bulk,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_0010)).transfer_type()
+        );
+        assert_eq!(
+            TransferType::Interrupt,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_0011)).transfer_type()
+        );
+    }
+
+    #[test]
+    fn it_interprets_synchronization_type_in_attributes() {
+        assert_eq!(
+            SyncType::NoSync,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_0001)).sync_type()
+        );
+        assert_eq!(
+            SyncType::Asynchronous,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_0101)).sync_type()
+        );
+        assert_eq!(
+            SyncType::Adaptive,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_1001)).sync_type()
+        );
+        assert_eq!(
+            SyncType::Synchronous,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_1101)).sync_type()
+        );
+    }
+
+    #[test]
+    fn it_interprets_usage_type_in_attributes() {
+        assert_eq!(
+            UsageType::Data,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0000_0001)).usage_type()
+        );
+        assert_eq!(
+            UsageType::Feedback,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0001_0001)).usage_type()
+        );
+        assert_eq!(
+            UsageType::FeedbackData,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0010_0001)).usage_type()
+        );
+        assert_eq!(
+            UsageType::Reserved,
+            super::from_libusb(&endpoint_descriptor!(bmAttributes: 0b0011_0001)).usage_type()
+        );
+    }
+
+    #[test]
+    fn it_has_max_packet_size() {
+        assert_eq!(
+            64,
+            super::from_libusb(&endpoint_descriptor!(wMaxPacketSize: 64)).max_packet_size()
+        );
+        assert_eq!(
+            4096,
+            super::from_libusb(&endpoint_descriptor!(wMaxPacketSize: 4096)).max_packet_size()
+        );
+        assert_eq!(
+            65535,
+            super::from_libusb(&endpoint_descriptor!(wMaxPacketSize: 65535)).max_packet_size()
+        );
+    }
+
+    #[test]
+    fn it_has_interval() {
+        assert_eq!(
+            1,
+            super::from_libusb(&endpoint_descriptor!(bInterval: 1)).interval()
+        );
+        assert_eq!(
+            20,
+            super::from_libusb(&endpoint_descriptor!(bInterval: 20)).interval()
+        );
+        assert_eq!(
+            255,
+            super::from_libusb(&endpoint_descriptor!(bInterval: 255)).interval()
+        );
+    }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..199b483
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,108 @@
+use std::{fmt, result};
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use libusb1_sys::constants::*;
+
+/// A result of a function that may return a `Error`.
+pub type Result<T> = result::Result<T, Error>;
+
+/// Errors returned by the `libusb` library.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum Error {
+    /// Input/output error.
+    Io,
+
+    /// Invalid parameter.
+    InvalidParam,
+
+    /// Access denied (insufficient permissions).
+    Access,
+
+    /// No such device (it may have been disconnected).
+    NoDevice,
+
+    /// Entity not found.
+    NotFound,
+
+    /// Resource busy.
+    Busy,
+
+    /// Operation timed out.
+    Timeout,
+
+    /// Overflow.
+    Overflow,
+
+    /// Pipe error.
+    Pipe,
+
+    /// System call interrupted (perhaps due to signal).
+    Interrupted,
+
+    /// Insufficient memory.
+    NoMem,
+
+    /// Operation not supported or unimplemented on this platform.
+    NotSupported,
+
+    /// The device returned a malformed descriptor.
+    BadDescriptor,
+
+    /// Other error.
+    Other,
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
+        fmt.write_str(match self {
+            Error::Io => "Input/Output Error",
+            Error::InvalidParam => "Invalid parameter",
+            Error::Access => "Access denied (insufficient permissions)",
+            Error::NoDevice => "No such device (it may have been disconnected)",
+            Error::NotFound => "Entity not found",
+            Error::Busy => "Resource busy",
+            Error::Timeout => "Operation timed out",
+            Error::Overflow => "Overflow",
+            Error::Pipe => "Pipe error",
+            Error::Interrupted => "System call interrupted (perhaps due to signal)",
+            Error::NoMem => "Insufficient memory",
+            Error::NotSupported => "Operation not supported or unimplemented on this platform",
+            Error::BadDescriptor => "Malformed descriptor",
+            Error::Other => "Other error",
+        })
+    }
+}
+
+impl std::error::Error for Error {}
+
+#[doc(hidden)]
+pub(crate) fn from_libusb(err: i32) -> Error {
+    match err {
+        LIBUSB_ERROR_IO => Error::Io,
+        LIBUSB_ERROR_INVALID_PARAM => Error::InvalidParam,
+        LIBUSB_ERROR_ACCESS => Error::Access,
+        LIBUSB_ERROR_NO_DEVICE => Error::NoDevice,
+        LIBUSB_ERROR_NOT_FOUND => Error::NotFound,
+        LIBUSB_ERROR_BUSY => Error::Busy,
+        LIBUSB_ERROR_TIMEOUT => Error::Timeout,
+        LIBUSB_ERROR_OVERFLOW => Error::Overflow,
+        LIBUSB_ERROR_PIPE => Error::Pipe,
+        LIBUSB_ERROR_INTERRUPTED => Error::Interrupted,
+        LIBUSB_ERROR_NO_MEM => Error::NoMem,
+        LIBUSB_ERROR_NOT_SUPPORTED => Error::NotSupported,
+        LIBUSB_ERROR_OTHER | _ => Error::Other,
+    }
+}
+
+#[doc(hidden)]
+macro_rules! try_unsafe {
+    ($x:expr) => {
+        match unsafe { $x } {
+            0 => (),
+            err => return Err($crate::error::from_libusb(err)),
+        }
+    };
+}
diff --git a/src/fields.rs b/src/fields.rs
new file mode 100644
index 0000000..2ead891
--- /dev/null
+++ b/src/fields.rs
@@ -0,0 +1,398 @@
+use libc::c_int;
+use libusb1_sys::constants::*;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+/// Device speeds. Indicates the speed at which a device is operating.
+/// - [libusb_supported_speed](http://libusb.sourceforge.net/api-1.0/group__libusb__dev.html#ga1454797ecc0de4d084c1619c420014f6)
+/// - [USB release versions](https://en.wikipedia.org/wiki/USB#Release_versions)
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[non_exhaustive]
+pub enum Speed {
+    /// The operating system doesn't know the device speed.
+    Unknown,
+
+    /// The device is operating at low speed (1.5 Mbps).
+    Low,
+
+    /// The device is operating at full speed (12 Mbps).
+    Full,
+
+    /// The device is operating at high speed (480 Mbps).
+    High,
+
+    /// The device is operating at super speed (5 Gbps).
+    Super,
+
+    /// The device is operating at super speed (10 Gbps).
+    SuperPlus,
+}
+
+#[doc(hidden)]
+pub(crate) fn speed_from_libusb(n: c_int) -> Speed {
+    match n {
+        LIBUSB_SPEED_SUPER_PLUS => Speed::SuperPlus,
+        LIBUSB_SPEED_SUPER => Speed::Super,
+        LIBUSB_SPEED_HIGH => Speed::High,
+        LIBUSB_SPEED_FULL => Speed::Full,
+        LIBUSB_SPEED_LOW => Speed::Low,
+
+        LIBUSB_SPEED_UNKNOWN | _ => Speed::Unknown,
+    }
+}
+
+/// Transfer and endpoint directions.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum Direction {
+    /// Direction for read (device to host) transfers.
+    In,
+
+    /// Direction for write (host to device) transfers.
+    Out,
+}
+
+/// An endpoint's transfer type.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum TransferType {
+    /// Control endpoint.
+    Control,
+
+    /// Isochronous endpoint.
+    Isochronous,
+
+    /// Bulk endpoint.
+    Bulk,
+
+    /// Interrupt endpoint.
+    Interrupt,
+}
+
+/// Isochronous synchronization mode.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum SyncType {
+    /// No synchronisation.
+    NoSync,
+
+    /// Asynchronous.
+    Asynchronous,
+
+    /// Adaptive.
+    Adaptive,
+
+    /// Synchronous.
+    Synchronous,
+}
+
+/// Isochronous usage type.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum UsageType {
+    /// Data endpoint.
+    Data,
+
+    /// Feedback endpoint.
+    Feedback,
+
+    /// Explicit feedback data endpoint.
+    FeedbackData,
+
+    /// Reserved.
+    Reserved,
+}
+
+/// Types of control transfers.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum RequestType {
+    /// Requests that are defined by the USB standard.
+    Standard,
+
+    /// Requests that are defined by a device class, e.g., HID.
+    Class,
+
+    /// Vendor-specific requests.
+    Vendor,
+
+    /// Reserved for future use.
+    Reserved,
+}
+
+/// Recipients of control transfers.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum Recipient {
+    /// The recipient is a device.
+    Device,
+
+    /// The recipient is an interface.
+    Interface,
+
+    /// The recipient is an endpoint.
+    Endpoint,
+
+    /// Other.
+    Other,
+}
+
+/// A three-part version consisting of major, minor, and sub minor components.
+///
+/// This can be used to represent versions of the format `J.M.N`, where `J` is the major version,
+/// `M` is the minor version, and `N` is the sub minor version. A version is constructed by
+/// providing the fields in the same order to the tuple. For example:
+///
+/// ```
+/// rusb::Version(0, 2, 1);
+/// ```
+///
+/// represents the version 0.2.1.
+///
+/// The intended use case of `Version` is to extract meaning from the version fields in USB
+/// descriptors, such as `bcdUSB` and `bcdDevice` in device descriptors.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Version(pub u8, pub u8, pub u8);
+
+impl Version {
+    /// Extracts a version from a binary coded decimal (BCD) field. BCD fields exist in USB
+    /// descriptors as 16-bit integers encoding a version as `0xJJMN`, where `JJ` is the major
+    /// version, `M` is the minor version, and `N` is the sub minor version. For example, 2.0 is
+    /// encoded as `0x0200` and 1.1 is encoded as `0x0110`.
+    pub fn from_bcd(mut raw: u16) -> Self {
+        let sub_minor: u8 = (raw & 0x000F) as u8;
+        raw >>= 4;
+
+        let minor: u8 = (raw & 0x000F) as u8;
+        raw >>= 4;
+
+        let mut major: u8 = (raw & 0x000F) as u8;
+        raw >>= 4;
+
+        major += (10 * raw) as u8;
+
+        Version(major, minor, sub_minor)
+    }
+
+    /// Returns the major version.
+    pub fn major(self) -> u8 {
+        let Version(major, _, _) = self;
+        major
+    }
+
+    /// Returns the minor version.
+    pub fn minor(self) -> u8 {
+        let Version(_, minor, _) = self;
+        minor
+    }
+
+    /// Returns the sub minor version.
+    pub fn sub_minor(self) -> u8 {
+        let Version(_, _, sub_minor) = self;
+        sub_minor
+    }
+}
+
+impl std::fmt::Display for Version {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}.{}.{}", self.major(), self.minor(), self.sub_minor())
+    }
+}
+
+/// Builds a value for the `bmRequestType` field of a control transfer setup packet.
+///
+/// The `bmRequestType` field of a USB control transfer setup packet is a bit field specifying
+/// three parameters, which are given to this function by corresponding enum values.
+///
+/// ## Examples
+///
+/// The following example returns a `bmRequestType` value for a standard inbound transfer from the
+/// device, which could be used for reading a device's descriptors:
+///
+/// ```no_run
+/// use rusb::{Direction,RequestType,Recipient};
+///
+/// rusb::request_type(Direction::In, RequestType::Standard, Recipient::Device);
+/// ```
+pub const fn request_type(
+    direction: Direction,
+    request_type: RequestType,
+    recipient: Recipient,
+) -> u8 {
+    let mut value: u8 = match direction {
+        Direction::Out => LIBUSB_ENDPOINT_OUT,
+        Direction::In => LIBUSB_ENDPOINT_IN,
+    };
+
+    value |= match request_type {
+        RequestType::Standard => LIBUSB_REQUEST_TYPE_STANDARD,
+        RequestType::Class => LIBUSB_REQUEST_TYPE_CLASS,
+        RequestType::Vendor => LIBUSB_REQUEST_TYPE_VENDOR,
+        RequestType::Reserved => LIBUSB_REQUEST_TYPE_RESERVED,
+    };
+
+    value |= match recipient {
+        Recipient::Device => LIBUSB_RECIPIENT_DEVICE,
+        Recipient::Interface => LIBUSB_RECIPIENT_INTERFACE,
+        Recipient::Endpoint => LIBUSB_RECIPIENT_ENDPOINT,
+        Recipient::Other => LIBUSB_RECIPIENT_OTHER,
+    };
+
+    value
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    // Version
+
+    #[test]
+    fn version_returns_major_version() {
+        assert_eq!(1, Version(1, 0, 0).major());
+        assert_eq!(2, Version(2, 0, 0).major());
+    }
+
+    #[test]
+    fn version_returns_minor_version() {
+        assert_eq!(1, Version(0, 1, 0).minor());
+        assert_eq!(2, Version(0, 2, 0).minor());
+    }
+
+    #[test]
+    fn version_returns_sub_minor_version() {
+        assert_eq!(1, Version(0, 0, 1).sub_minor());
+        assert_eq!(2, Version(0, 0, 2).sub_minor());
+    }
+
+    #[test]
+    fn version_parses_major_version() {
+        assert_eq!(3, Version::from_bcd(0x0300).major());
+    }
+
+    #[test]
+    fn version_parses_long_major_version() {
+        assert_eq!(12, Version::from_bcd(0x1200).major());
+    }
+
+    #[test]
+    fn version_parses_minor_version() {
+        assert_eq!(1, Version::from_bcd(0x0010).minor());
+        assert_eq!(2, Version::from_bcd(0x0020).minor());
+    }
+
+    #[test]
+    fn version_parses_sub_minor_version() {
+        assert_eq!(1, Version::from_bcd(0x0001).sub_minor());
+        assert_eq!(2, Version::from_bcd(0x0002).sub_minor());
+    }
+
+    #[test]
+    fn version_parses_full_version() {
+        assert_eq!(Version(12, 3, 4), Version::from_bcd(0x1234));
+    }
+
+    #[test]
+    fn version_display() {
+        assert_eq!(Version(2, 45, 13).to_string(), "2.45.13");
+    }
+
+    #[test]
+    fn version_ord() {
+        assert!(Version(0, 0, 0) < Version(1, 2, 3));
+        assert!(Version(1, 0, 0) < Version(1, 2, 3));
+        assert!(Version(1, 2, 0) < Version(1, 2, 3));
+        assert!(Version(1, 2, 0) < Version(1, 3, 0));
+        assert!(Version(255, 255, 255) > Version(254, 0, 0));
+        assert!(Version(0, 255, 0) > Version(0, 254, 255));
+    }
+
+    // request_type for direction
+
+    #[test]
+    fn request_type_builds_value_for_out_direction() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Standard, Recipient::Device) & 0x80,
+            0x00
+        );
+    }
+
+    #[test]
+    fn request_type_builds_value_for_in_direction() {
+        assert_eq!(
+            request_type(Direction::In, RequestType::Standard, Recipient::Device) & 0x80,
+            0x80
+        );
+    }
+
+    // request_type for request type
+
+    #[test]
+    fn request_type_builds_value_for_standard_request() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Standard, Recipient::Device) & 0x60,
+            0x00
+        );
+    }
+
+    #[test]
+    fn request_type_builds_value_for_class_request() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Class, Recipient::Device) & 0x60,
+            0x20
+        );
+    }
+
+    #[test]
+    fn request_type_builds_value_for_vendor_request() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Vendor, Recipient::Device) & 0x60,
+            0x40
+        );
+    }
+
+    #[test]
+    fn request_type_builds_value_for_reserved_request() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Reserved, Recipient::Device) & 0x60,
+            0x60
+        );
+    }
+
+    // request_type for recipient
+
+    #[test]
+    fn request_type_builds_value_for_device_recipient() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Standard, Recipient::Device) & 0x0F,
+            0x00
+        );
+    }
+
+    #[test]
+    fn request_type_builds_value_for_interface_recipient() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Standard, Recipient::Interface) & 0x0F,
+            0x01
+        );
+    }
+
+    #[test]
+    fn request_type_builds_value_for_endpoint_recipient() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Standard, Recipient::Endpoint) & 0x0F,
+            0x02
+        );
+    }
+
+    #[test]
+    fn request_type_builds_value_for_other_recipient() {
+        assert_eq!(
+            request_type(Direction::Out, RequestType::Standard, Recipient::Other) & 0x0F,
+            0x03
+        );
+    }
+}
diff --git a/src/hotplug.rs b/src/hotplug.rs
new file mode 100644
index 0000000..e067ffe
--- /dev/null
+++ b/src/hotplug.rs
@@ -0,0 +1,211 @@
+use crate::constants::{
+    LIBUSB_HOTPLUG_ENUMERATE, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
+    LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_NO_FLAGS,
+};
+use crate::ffi::{
+    libusb_context, libusb_device, libusb_hotplug_callback_handle,
+    libusb_hotplug_deregister_callback, libusb_hotplug_event, libusb_hotplug_register_callback,
+};
+use crate::{error, Device, UsbContext};
+use std::{
+    borrow::Borrow,
+    ffi::c_void,
+    fmt::{self, Debug},
+    os::raw::c_int,
+};
+
+/// When handling a [method@Hotplug::device_arrived] event it is considered safe to call
+/// any `rusb` function that takes a [`Device`]. It also safe to open a device and
+/// submit **asynchronous** transfers.
+/// However, most other functions that take a [`DeviceHandle`] are **not safe** to call.
+/// Examples of such functions are any of the synchronous API functions or
+/// the blocking functions that retrieve various USB descriptors.
+/// These functions must be used outside of the context of the [Hotplug] functions.
+///
+/// [`Device`]: crate::Device
+/// [`DeviceHandle`]: crate::DeviceHandle
+/// [`Context::unregister_callback`]: method@crate::Context::unregister_callback
+pub trait Hotplug<T: UsbContext>: Send {
+    fn device_arrived(&mut self, device: Device<T>);
+    fn device_left(&mut self, device: Device<T>);
+}
+
+#[derive(Debug)]
+#[must_use = "USB hotplug callbacks will be deregistered if the registration is dropped"]
+pub struct Registration<T: UsbContext> {
+    handle: libusb_hotplug_callback_handle,
+    call_back: Box<CallbackData<T>>,
+}
+
+impl<T: UsbContext> Registration<T> {
+    fn get_handle(&self) -> libusb_hotplug_callback_handle {
+        self.handle
+    }
+}
+
+impl<T: UsbContext> Drop for Registration<T> {
+    fn drop(&mut self) {
+        unsafe {
+            libusb_hotplug_deregister_callback(self.call_back.context.as_raw(), self.get_handle())
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default)]
+#[doc(alias = "libusb_hotplug_register_callback")]
+/// Builds hotplug [Registration] with custom configuration values.
+pub struct HotplugBuilder {
+    vendor_id: Option<u16>,
+    product_id: Option<u16>,
+    class: Option<u8>,
+    enumerate: bool,
+}
+
+impl HotplugBuilder {
+    /// Returns a new builder with the no filter
+    /// Devices can optionally be filtered by [HotplugBuilder::vendor_id]
+    /// and [HotplugBuilder::product_id]
+    ///
+    /// Registration is done by by calling [`register`].
+    ///
+    /// [`register`]: method@Self::register
+    pub fn new() -> Self {
+        HotplugBuilder {
+            vendor_id: None,
+            product_id: None,
+            class: None,
+            enumerate: false,
+        }
+    }
+
+    /// Devices can optionally be filtered by vendor
+    pub fn vendor_id(&mut self, vendor_id: u16) -> &mut Self {
+        self.vendor_id = Some(vendor_id);
+        self
+    }
+
+    /// Devices can optionally be filtered by product id
+    pub fn product_id(&mut self, product_id: u16) -> &mut Self {
+        self.product_id = Some(product_id);
+        self
+    }
+
+    /// Devices can optionally be filtered by class
+    pub fn class(&mut self, class: u8) -> &mut Self {
+        self.class = Some(class);
+        self
+    }
+
+    /// If `enumerate` is `true`, then devices that are already
+    /// connected will cause your callback's [Hotplug::device_arrived] method to be
+    /// called for them.
+    pub fn enumerate(&mut self, enumerate: bool) -> &mut Self {
+        self.enumerate = enumerate;
+        self
+    }
+
+    /// Register a `callback` to be called on hotplug events. The callback's
+    /// [method@Hotplug::device_arrived] method is called when a new device is added to
+    /// the bus, and [method@Hotplug::device_left] is called when it is removed.
+    ///
+    /// The callback will remain registered until the returned [Registration] is
+    /// dropped, which can be done explicitly with [`Context::unregister_callback`].
+    ///
+    /// When handling a [method@Hotplug::device_arrived] event it is considered safe to call
+    /// any `rusb` function that takes a [`Device`]. It also safe to open a device and
+    /// submit **asynchronous** transfers.
+    /// However, most other functions that take a [`DeviceHandle`] are **not safe** to call.
+    /// Examples of such functions are any of the synchronous API functions or
+    /// the blocking functions that retrieve various USB descriptors.
+    /// These functions must be used outside of the context of the [Hotplug] functions.
+    ///
+    /// [`Device`]: crate::Device
+    /// [`DeviceHandle`]: crate::DeviceHandle
+    /// [`Context::unregister_callback`]: method@crate::Context::unregister_callback
+    pub fn register<U: UsbContext, T: Borrow<U>>(
+        self,
+        context: T,
+        callback: Box<dyn Hotplug<U>>,
+    ) -> crate::Result<Registration<U>> {
+        let mut handle: libusb_hotplug_callback_handle = 0;
+        let mut call_back = Box::new(CallbackData {
+            context: context.borrow().clone(),
+            hotplug: callback,
+        });
+
+        let hotplug_flags = if self.enumerate {
+            LIBUSB_HOTPLUG_ENUMERATE
+        } else {
+            LIBUSB_HOTPLUG_NO_FLAGS
+        };
+
+        let user_data = &mut *call_back as *mut _ as *mut _;
+
+        let n = unsafe {
+            libusb_hotplug_register_callback(
+                context.borrow().as_raw(),
+                LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
+                hotplug_flags,
+                self.vendor_id
+                    .map(c_int::from)
+                    .unwrap_or(LIBUSB_HOTPLUG_MATCH_ANY),
+                self.product_id
+                    .map(c_int::from)
+                    .unwrap_or(LIBUSB_HOTPLUG_MATCH_ANY),
+                self.class
+                    .map(c_int::from)
+                    .unwrap_or(LIBUSB_HOTPLUG_MATCH_ANY),
+                hotplug_callback::<U>,
+                user_data,
+                &mut handle,
+            )
+        };
+        if n < 0 {
+            Err(error::from_libusb(n))
+        } else {
+            Ok(Registration { handle, call_back })
+        }
+    }
+}
+
+struct CallbackData<T: UsbContext> {
+    context: T,
+    hotplug: Box<dyn Hotplug<T>>,
+}
+
+impl<T> Debug for CallbackData<T>
+where
+    T: UsbContext + Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("CallbackData")
+            .field("context", &self.context)
+            .finish()
+    }
+}
+
+pub extern "system" fn hotplug_callback<T: UsbContext>(
+    _ctx: *mut libusb_context,
+    device: *mut libusb_device,
+    event: libusb_hotplug_event,
+    user_data: *mut c_void,
+) -> c_int {
+    let ret = std::panic::catch_unwind(|| {
+        let reg = unsafe { &mut *(user_data as *mut CallbackData<T>) };
+        let device = unsafe {
+            Device::from_libusb(
+                reg.context.clone(),
+                std::ptr::NonNull::new_unchecked(device),
+            )
+        };
+        match event {
+            LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED => reg.hotplug.device_arrived(device),
+            LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT => reg.hotplug.device_left(device),
+            _ => (),
+        };
+    });
+    match ret {
+        Ok(_) => 0,
+        Err(_) => 1,
+    }
+}
diff --git a/src/interface_descriptor.rs b/src/interface_descriptor.rs
new file mode 100644
index 0000000..d4c64c3
--- /dev/null
+++ b/src/interface_descriptor.rs
@@ -0,0 +1,294 @@
+use std::{fmt, slice};
+
+use libusb1_sys::{libusb_endpoint_descriptor, libusb_interface, libusb_interface_descriptor};
+
+use crate::endpoint_descriptor::{self, EndpointDescriptor};
+
+/// A device interface.
+///
+/// An interface can have several descriptors, each describing an alternate setting of the
+/// interface.
+pub struct Interface<'a> {
+    descriptors: &'a [libusb_interface_descriptor],
+}
+
+impl<'a> Interface<'a> {
+    /// Returns the interface's number.
+    pub fn number(&self) -> u8 {
+        self.descriptors[0].bInterfaceNumber
+    }
+
+    /// Returns an iterator over the interface's descriptors.
+    pub fn descriptors(&self) -> InterfaceDescriptors<'a> {
+        InterfaceDescriptors {
+            iter: self.descriptors.iter(),
+        }
+    }
+}
+
+/// Iterator over an interface's descriptors.
+pub struct InterfaceDescriptors<'a> {
+    iter: slice::Iter<'a, libusb_interface_descriptor>,
+}
+
+impl<'a> Iterator for InterfaceDescriptors<'a> {
+    type Item = InterfaceDescriptor<'a>;
+
+    fn next(&mut self) -> Option<InterfaceDescriptor<'a>> {
+        self.iter
+            .next()
+            .map(|descriptor| InterfaceDescriptor { descriptor })
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.iter.size_hint()
+    }
+}
+
+/// Describes an alternate setting for an interface.
+pub struct InterfaceDescriptor<'a> {
+    descriptor: &'a libusb_interface_descriptor,
+}
+
+impl<'a> InterfaceDescriptor<'a> {
+    /// Returns the size of the descriptor in bytes
+    pub fn length(&self) -> u8 {
+        self.descriptor.bLength
+    }
+
+    /// Returns the descriptor type
+    pub fn descriptor_type(&self) -> u8 {
+        self.descriptor.bDescriptorType
+    }
+
+    /// Returns the interface's number.
+    pub fn interface_number(&self) -> u8 {
+        self.descriptor.bInterfaceNumber
+    }
+
+    /// Returns the alternate setting number.
+    pub fn setting_number(&self) -> u8 {
+        self.descriptor.bAlternateSetting
+    }
+
+    /// Returns the interface's class code.
+    pub fn class_code(&self) -> u8 {
+        self.descriptor.bInterfaceClass
+    }
+
+    /// Returns the interface's sub class code.
+    pub fn sub_class_code(&self) -> u8 {
+        self.descriptor.bInterfaceSubClass
+    }
+
+    /// Returns the interface's protocol code.
+    pub fn protocol_code(&self) -> u8 {
+        self.descriptor.bInterfaceProtocol
+    }
+
+    /// Returns the index of the string descriptor that describes the interface.
+    pub fn description_string_index(&self) -> Option<u8> {
+        match self.descriptor.iInterface {
+            0 => None,
+            n => Some(n),
+        }
+    }
+
+    /// Returns the number of endpoints belonging to this interface.
+    pub fn num_endpoints(&self) -> u8 {
+        self.descriptor.bNumEndpoints
+    }
+
+    /// Returns an iterator over the interface's endpoint descriptors.
+    pub fn endpoint_descriptors(&self) -> EndpointDescriptors<'a> {
+        let endpoints = match self.descriptor.bNumEndpoints {
+            0 => &[],
+            n => unsafe { slice::from_raw_parts(self.descriptor.endpoint, n as usize) },
+        };
+
+        EndpointDescriptors {
+            iter: endpoints.iter(),
+        }
+    }
+
+    /// Returns the unknown 'extra' bytes that libusb does not understand.
+    pub fn extra(&self) -> &[u8] {
+        unsafe {
+            match (*self.descriptor).extra_length {
+                len if len > 0 => slice::from_raw_parts((*self.descriptor).extra, len as usize),
+                _ => &[],
+            }
+        }
+    }
+}
+
+impl<'a> fmt::Debug for InterfaceDescriptor<'a> {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        let mut debug = fmt.debug_struct("InterfaceDescriptor");
+
+        debug.field("bLength", &self.descriptor.bLength);
+        debug.field("bDescriptorType", &self.descriptor.bDescriptorType);
+        debug.field("bInterfaceNumber", &self.descriptor.bInterfaceNumber);
+        debug.field("bAlternateSetting", &self.descriptor.bAlternateSetting);
+        debug.field("bNumEndpoints", &self.descriptor.bNumEndpoints);
+        debug.field("bInterfaceClass", &self.descriptor.bInterfaceClass);
+        debug.field("bInterfaceSubClass", &self.descriptor.bInterfaceSubClass);
+        debug.field("bInterfaceProtocol", &self.descriptor.bInterfaceProtocol);
+        debug.field("iInterface", &self.descriptor.iInterface);
+
+        debug.finish()
+    }
+}
+
+/// Iterator over an interface's endpoint descriptors.
+pub struct EndpointDescriptors<'a> {
+    iter: slice::Iter<'a, libusb_endpoint_descriptor>,
+}
+
+impl<'a> Iterator for EndpointDescriptors<'a> {
+    type Item = EndpointDescriptor<'a>;
+
+    fn next(&mut self) -> Option<EndpointDescriptor<'a>> {
+        self.iter.next().map(endpoint_descriptor::from_libusb)
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.iter.size_hint()
+    }
+}
+
+#[doc(hidden)]
+pub(crate) unsafe fn from_libusb(interface: &libusb_interface) -> Interface {
+    let descriptors =
+        slice::from_raw_parts(interface.altsetting, interface.num_altsetting as usize);
+    debug_assert!(!descriptors.is_empty());
+
+    Interface { descriptors }
+}
+
+#[cfg(test)]
+mod test {
+    #[test]
+    fn it_has_interface_number() {
+        assert_eq!(
+            42,
+            unsafe { super::from_libusb(&interface!(interface_descriptor!(bInterfaceNumber: 42))) }
+                .number()
+        );
+    }
+
+    #[test]
+    fn it_has_interface_number_in_descriptor() {
+        assert_eq!(
+            vec!(42),
+            unsafe { super::from_libusb(&interface!(interface_descriptor!(bInterfaceNumber: 42))) }
+                .descriptors()
+                .map(|setting| setting.interface_number())
+                .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_has_alternate_setting_number() {
+        assert_eq!(
+            vec!(42),
+            unsafe {
+                super::from_libusb(&interface!(interface_descriptor!(bAlternateSetting: 42)))
+            }
+            .descriptors()
+            .map(|setting| setting.setting_number())
+            .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_has_class_code() {
+        assert_eq!(
+            vec!(42),
+            unsafe { super::from_libusb(&interface!(interface_descriptor!(bInterfaceClass: 42))) }
+                .descriptors()
+                .map(|setting| setting.class_code())
+                .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_has_sub_class_code() {
+        assert_eq!(
+            vec!(42),
+            unsafe {
+                super::from_libusb(&interface!(interface_descriptor!(bInterfaceSubClass: 42)))
+            }
+            .descriptors()
+            .map(|setting| setting.sub_class_code())
+            .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_has_protocol_code() {
+        assert_eq!(
+            vec!(42),
+            unsafe {
+                super::from_libusb(&interface!(interface_descriptor!(bInterfaceProtocol: 42)))
+            }
+            .descriptors()
+            .map(|setting| setting.protocol_code())
+            .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_has_description_string_index() {
+        assert_eq!(
+            vec!(Some(42)),
+            unsafe { super::from_libusb(&interface!(interface_descriptor!(iInterface: 42))) }
+                .descriptors()
+                .map(|setting| setting.description_string_index())
+                .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_handles_missing_description_string_index() {
+        assert_eq!(
+            vec!(None),
+            unsafe { super::from_libusb(&interface!(interface_descriptor!(iInterface: 0))) }
+                .descriptors()
+                .map(|setting| setting.description_string_index())
+                .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_has_num_endpoints() {
+        let endpoint1 = endpoint_descriptor!(bEndpointAddress: 0x81);
+        let endpoint2 = endpoint_descriptor!(bEndpointAddress: 0x01);
+
+        assert_eq!(
+            vec!(2),
+            unsafe { super::from_libusb(&interface!(interface_descriptor!(endpoint1, endpoint2))) }
+                .descriptors()
+                .map(|setting| setting.num_endpoints())
+                .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn it_has_endpoints() {
+        let libusb_interface = interface!(interface_descriptor!(
+            endpoint_descriptor!(bEndpointAddress: 0x87)
+        ));
+        let interface = unsafe { super::from_libusb(&libusb_interface) };
+
+        let endpoint_addresses = interface
+            .descriptors()
+            .next()
+            .unwrap()
+            .endpoint_descriptors()
+            .map(|endpoint| endpoint.address())
+            .collect::<Vec<_>>();
+
+        assert_eq!(vec![0x87], endpoint_addresses);
+    }
+}
diff --git a/src/language.rs b/src/language.rs
new file mode 100644
index 0000000..4fec0d1
--- /dev/null
+++ b/src/language.rs
@@ -0,0 +1,2558 @@
+const PRIMARY_LANGUAGE_MASK: u16 = 0x03FF;
+const SUB_LANGUAGE_MASK: u16 = 0xFC00;
+
+/// A language used to read string descriptors from USB devices.
+///
+/// A language consists of a primary language and a sub language. Primary languages are language
+/// families, such as English or Spanish. Sub languages identify a dialect of the primary language.
+/// The dialect may be based on regional differences (United States English compared to United
+/// Kingdom English), writing systems (Cyrillic compared to Latin), or age (Modern compared to
+/// Traditional). Each primary language has its own set of sub languages.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct Language {
+    raw: u16,
+}
+
+impl Language {
+    /// Returns the language's 16-bit `LANGID`.
+    ///
+    /// Each language's `LANGID` is defined by the USB forum
+    /// <https://learn.microsoft.com/en-us/windows/win32/intl/language-identifier-constants-and-strings>.
+    pub fn lang_id(self) -> u16 {
+        self.raw
+    }
+
+    /// Returns the primary language.
+    pub fn primary_language(self) -> PrimaryLanguage {
+        PrimaryLanguage::from_raw(self.raw)
+    }
+
+    /// Returns the sub language.
+    pub fn sub_language(self) -> SubLanguage {
+        SubLanguage::from_raw(self.primary_language(), self.raw)
+    }
+}
+
+#[doc(hidden)]
+pub(crate) fn from_lang_id(raw: u16) -> Language {
+    Language { raw }
+}
+
+/// Primary language families.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum PrimaryLanguage {
+    Afrikaans,
+    Albanian,
+    Arabic,
+    Armenian,
+    Assamese,
+    Azeri,
+    Basque,
+    Belarussian,
+    Bengali,
+    Bulgarian,
+    Burmese,
+    Catalan,
+    Chinese,
+    Croatian,
+    Czech,
+    Danish,
+    Dutch,
+    English,
+    Estonian,
+    Faeroese,
+    Farsi,
+    Finnish,
+    French,
+    Georgian,
+    German,
+    Greek,
+    Gujarati,
+    Hebrew,
+    Hindi,
+    Hungarian,
+    Icelandic,
+    Indonesian,
+    Italian,
+    Japanese,
+    Kannada,
+    Kashmiri,
+    Kazakh,
+    Konkani,
+    Korean,
+    Latvian,
+    Lithuanian,
+    Macedonian,
+    Malay,
+    Malayalam,
+    Manipuri,
+    Marathi,
+    Nepali,
+    Norwegian,
+    Oriya,
+    Polish,
+    Portuguese,
+    Punjabi,
+    Romanian,
+    Russian,
+    Sanskrit,
+    Serbian,
+    Sindhi,
+    Slovak,
+    Slovenian,
+    Spanish,
+    Sutu,
+    Swahili,
+    Swedish,
+    Tamil,
+    Tatar,
+    Telugu,
+    Thai,
+    Turkish,
+    Ukrainian,
+    Urdu,
+    Uzbek,
+    Vietnamese,
+
+    HID,
+    Other(u16),
+}
+
+impl PrimaryLanguage {
+    fn from_raw(raw: u16) -> PrimaryLanguage {
+        match raw & PRIMARY_LANGUAGE_MASK {
+            0x0036 => PrimaryLanguage::Afrikaans,
+            0x001C => PrimaryLanguage::Albanian,
+            0x0001 => PrimaryLanguage::Arabic,
+            0x002B => PrimaryLanguage::Armenian,
+            0x004D => PrimaryLanguage::Assamese,
+            0x002C => PrimaryLanguage::Azeri,
+            0x002D => PrimaryLanguage::Basque,
+            0x0023 => PrimaryLanguage::Belarussian,
+            0x0045 => PrimaryLanguage::Bengali,
+            0x0002 => PrimaryLanguage::Bulgarian,
+            0x0055 => PrimaryLanguage::Burmese,
+            0x0003 => PrimaryLanguage::Catalan,
+            0x0004 => PrimaryLanguage::Chinese,
+            0x001A => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => PrimaryLanguage::Croatian,
+                _ => PrimaryLanguage::Serbian,
+            },
+            0x0005 => PrimaryLanguage::Czech,
+            0x0006 => PrimaryLanguage::Danish,
+            0x0013 => PrimaryLanguage::Dutch,
+            0x0009 => PrimaryLanguage::English,
+            0x0025 => PrimaryLanguage::Estonian,
+            0x0038 => PrimaryLanguage::Faeroese,
+            0x0029 => PrimaryLanguage::Farsi,
+            0x000B => PrimaryLanguage::Finnish,
+            0x000C => PrimaryLanguage::French,
+            0x0037 => PrimaryLanguage::Georgian,
+            0x0007 => PrimaryLanguage::German,
+            0x0008 => PrimaryLanguage::Greek,
+            0x0047 => PrimaryLanguage::Gujarati,
+            0x000D => PrimaryLanguage::Hebrew,
+            0x0039 => PrimaryLanguage::Hindi,
+            0x000E => PrimaryLanguage::Hungarian,
+            0x000F => PrimaryLanguage::Icelandic,
+            0x0021 => PrimaryLanguage::Indonesian,
+            0x0010 => PrimaryLanguage::Italian,
+            0x0011 => PrimaryLanguage::Japanese,
+            0x004B => PrimaryLanguage::Kannada,
+            0x0060 => PrimaryLanguage::Kashmiri,
+            0x003F => PrimaryLanguage::Kazakh,
+            0x0057 => PrimaryLanguage::Konkani,
+            0x0012 => PrimaryLanguage::Korean,
+            0x0026 => PrimaryLanguage::Latvian,
+            0x0027 => PrimaryLanguage::Lithuanian,
+            0x002F => PrimaryLanguage::Macedonian,
+            0x003E => PrimaryLanguage::Malay,
+            0x004C => PrimaryLanguage::Malayalam,
+            0x0058 => PrimaryLanguage::Manipuri,
+            0x004E => PrimaryLanguage::Marathi,
+            0x0061 => PrimaryLanguage::Nepali,
+            0x0014 => PrimaryLanguage::Norwegian,
+            0x0048 => PrimaryLanguage::Oriya,
+            0x0015 => PrimaryLanguage::Polish,
+            0x0016 => PrimaryLanguage::Portuguese,
+            0x0046 => PrimaryLanguage::Punjabi,
+            0x0018 => PrimaryLanguage::Romanian,
+            0x0019 => PrimaryLanguage::Russian,
+            0x004F => PrimaryLanguage::Sanskrit,
+            0x0059 => PrimaryLanguage::Sindhi,
+            0x001B => PrimaryLanguage::Slovak,
+            0x0024 => PrimaryLanguage::Slovenian,
+            0x000A => PrimaryLanguage::Spanish,
+            0x0030 => PrimaryLanguage::Sutu,
+            0x0041 => PrimaryLanguage::Swahili,
+            0x001D => PrimaryLanguage::Swedish,
+            0x0049 => PrimaryLanguage::Tamil,
+            0x0044 => PrimaryLanguage::Tatar,
+            0x004A => PrimaryLanguage::Telugu,
+            0x001E => PrimaryLanguage::Thai,
+            0x001F => PrimaryLanguage::Turkish,
+            0x0022 => PrimaryLanguage::Ukrainian,
+            0x0020 => PrimaryLanguage::Urdu,
+            0x0043 => PrimaryLanguage::Uzbek,
+            0x002A => PrimaryLanguage::Vietnamese,
+            0x00FF => PrimaryLanguage::HID,
+            n => PrimaryLanguage::Other(n),
+        }
+    }
+}
+
+/// Language dialects and writing systems.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum SubLanguage {
+    Standard,
+    Classic,
+    Traditional,
+    Modern,
+
+    Algeria,            // arabic
+    Argentina,          // spanish
+    Australia,          // english
+    Austria,            // german
+    Bahrain,            // arabic
+    Belgium,            // dutch, french
+    Belize,             // english
+    Bokmal,             // norwegian
+    Bolivia,            // spanish
+    Brazil,             // portuguese
+    BruneiDarussalam,   // malay
+    Canada,             // english, french
+    Caribbean,          // english
+    Chile,              // spanish
+    China,              // chinese
+    Colombia,           // spanish
+    CostaRica,          // spanish
+    Cyrillic,           // azeri, serbian, uzbek
+    DominicanRepublic,  // spanish
+    Ecuador,            // spanish
+    Egypt,              // arabic
+    ElSalvador,         // spanish
+    Finland,            // swedish
+    Guatemala,          // spanish
+    Honduras,           // spanish
+    HongKong,           // chinese
+    India,              // kashmiri, nepali, urdu
+    Iraq,               // arabic
+    Ireland,            // english
+    Jamaica,            // english
+    Johab,              // korean
+    Jordan,             // arabic
+    Kuwait,             // arabic
+    Latin,              // azeri, serbian, uzbek
+    Lebanon,            // arabic
+    Libya,              // arabic
+    Liechtenstein,      // german
+    Luxembourg,         // french, german
+    Macau,              // chinese
+    Malaysia,           // malay
+    Mexico,             // spanish
+    Monaco,             // french
+    Morocco,            // arabic
+    Netherlands,        // dutch
+    NewZealand,         // english
+    Nicaragua,          // spanish
+    Nynorsk,            // norwegian
+    Oman,               // arabic
+    Pakistan,           // urdu
+    Panama,             // spanish
+    Paraguay,           // spanish
+    Peru,               // spanish
+    Philippines,        // english
+    PuertoRico,         // spanish
+    Qatar,              // arabic
+    SaudiArabia,        // arabic
+    Singapore,          // chinese
+    SouthAfrica,        // english
+    Switzerland,        // french, german, italian
+    Syria,              // arabic
+    Taiwan,             // chinese
+    Trinidad,           // english
+    Tunisia,            // arabic
+    UnitedArabEmirates, // arabic
+    UnitedKingdom,      // english
+    UnitedStates,       // english
+    Uruguay,            // spanish
+    Venezuela,          // spanish
+    Yemen,              // arabic
+    Zimbabwe,           // english
+
+    UsageDataDescriptor, // HID
+    VendorDefined1,      // HID
+    VendorDefined2,      // HID
+    VendorDefined3,      // HID
+    VendorDefined4,      // HID
+
+    Other(u16),
+}
+
+impl SubLanguage {
+    fn from_raw(language: PrimaryLanguage, raw: u16) -> SubLanguage {
+        match language {
+            PrimaryLanguage::Arabic => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::SaudiArabia,
+                0x0800 => SubLanguage::Iraq,
+                0x0C00 => SubLanguage::Egypt,
+                0x1000 => SubLanguage::Libya,
+                0x1400 => SubLanguage::Algeria,
+                0x1800 => SubLanguage::Morocco,
+                0x1C00 => SubLanguage::Tunisia,
+                0x2000 => SubLanguage::Oman,
+                0x2400 => SubLanguage::Yemen,
+                0x2800 => SubLanguage::Syria,
+                0x2C00 => SubLanguage::Jordan,
+                0x3000 => SubLanguage::Lebanon,
+                0x3400 => SubLanguage::Kuwait,
+                0x3800 => SubLanguage::UnitedArabEmirates,
+                0x3C00 => SubLanguage::Bahrain,
+                0x4000 => SubLanguage::Qatar,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Azeri => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Latin,
+                0x0800 => SubLanguage::Cyrillic,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Chinese => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Taiwan,
+                0x0800 => SubLanguage::China,
+                0x0C00 => SubLanguage::HongKong,
+                0x1000 => SubLanguage::Singapore,
+                0x1400 => SubLanguage::Macau,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Dutch => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Netherlands,
+                0x0800 => SubLanguage::Belgium,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::English => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::UnitedStates,
+                0x0800 => SubLanguage::UnitedKingdom,
+                0x0C00 => SubLanguage::Australia,
+                0x1000 => SubLanguage::Canada,
+                0x1400 => SubLanguage::NewZealand,
+                0x1800 => SubLanguage::Ireland,
+                0x1C00 => SubLanguage::SouthAfrica,
+                0x2000 => SubLanguage::Jamaica,
+                0x2400 => SubLanguage::Caribbean,
+                0x2800 => SubLanguage::Belize,
+                0x2C00 => SubLanguage::Trinidad,
+                0x3000 => SubLanguage::Zimbabwe,
+                0x3400 => SubLanguage::Philippines,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::French => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Standard,
+                0x0800 => SubLanguage::Belgium,
+                0x0C00 => SubLanguage::Canada,
+                0x1000 => SubLanguage::Switzerland,
+                0x1400 => SubLanguage::Luxembourg,
+                0x1800 => SubLanguage::Monaco,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::German => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Standard,
+                0x0800 => SubLanguage::Switzerland,
+                0x0C00 => SubLanguage::Austria,
+                0x1000 => SubLanguage::Luxembourg,
+                0x1400 => SubLanguage::Liechtenstein,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Italian => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Standard,
+                0x0800 => SubLanguage::Switzerland,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Korean => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Standard,
+                0x0800 => SubLanguage::Johab,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Lithuanian => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Standard,
+                0x0800 => SubLanguage::Classic,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Malay => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Malaysia,
+                0x0800 => SubLanguage::BruneiDarussalam,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Norwegian => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Bokmal,
+                0x0800 => SubLanguage::Nynorsk,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Portuguese => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Brazil,
+                0x0800 => SubLanguage::Standard,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Serbian => match raw & SUB_LANGUAGE_MASK {
+                0x0C00 => SubLanguage::Cyrillic,
+                0x0800 => SubLanguage::Latin,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Spanish => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Traditional,
+                0x0800 => SubLanguage::Mexico,
+                0x0C00 => SubLanguage::Modern,
+                0x1000 => SubLanguage::Guatemala,
+                0x1400 => SubLanguage::CostaRica,
+                0x1800 => SubLanguage::Panama,
+                0x1C00 => SubLanguage::DominicanRepublic,
+                0x2000 => SubLanguage::Venezuela,
+                0x2400 => SubLanguage::Colombia,
+                0x2800 => SubLanguage::Peru,
+                0x2C00 => SubLanguage::Argentina,
+                0x3000 => SubLanguage::Ecuador,
+                0x3400 => SubLanguage::Chile,
+                0x3800 => SubLanguage::Uruguay,
+                0x3C00 => SubLanguage::Paraguay,
+                0x4000 => SubLanguage::Bolivia,
+                0x4400 => SubLanguage::ElSalvador,
+                0x4800 => SubLanguage::Honduras,
+                0x4C00 => SubLanguage::Nicaragua,
+                0x5000 => SubLanguage::PuertoRico,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Swedish => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Standard,
+                0x0800 => SubLanguage::Finland,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Urdu => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Pakistan,
+                0x0800 => SubLanguage::India,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Uzbek => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::Latin,
+                0x0800 => SubLanguage::Cyrillic,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::HID => match raw & SUB_LANGUAGE_MASK {
+                0x0400 => SubLanguage::UsageDataDescriptor,
+                0xF000 => SubLanguage::VendorDefined1,
+                0xF400 => SubLanguage::VendorDefined2,
+                0xF800 => SubLanguage::VendorDefined3,
+                0xFC00 => SubLanguage::VendorDefined4,
+                n => SubLanguage::Other(n),
+            },
+            PrimaryLanguage::Other(_) => SubLanguage::Other(raw & SUB_LANGUAGE_MASK),
+            _ => SubLanguage::Standard,
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{PrimaryLanguage, SubLanguage};
+    use super::{PRIMARY_LANGUAGE_MASK, SUB_LANGUAGE_MASK};
+
+    // language ids defined in http://www.usb.org/developers/docs/USB_LANGIDs.pdf
+    const AFRIKAANS: u16 = 0x0436;
+    const ALBANIAN: u16 = 0x041C;
+    const ARABIC_SAUDI_ARABIA: u16 = 0x0401;
+    const ARABIC_IRAQ: u16 = 0x0801;
+    const ARABIC_EGYPT: u16 = 0x0C01;
+    const ARABIC_LIBYA: u16 = 0x1001;
+    const ARABIC_ALGERIA: u16 = 0x1401;
+    const ARABIC_MOROCCO: u16 = 0x1801;
+    const ARABIC_TUNISIA: u16 = 0x1C01;
+    const ARABIC_OMAN: u16 = 0x2001;
+    const ARABIC_YEMEN: u16 = 0x2401;
+    const ARABIC_SYRIA: u16 = 0x2801;
+    const ARABIC_JORDAN: u16 = 0x2C01;
+    const ARABIC_LEBANON: u16 = 0x3001;
+    const ARABIC_KUWAIT: u16 = 0x3401;
+    const ARABIC_UAE: u16 = 0x3801;
+    const ARABIC_BAHRAIN: u16 = 0x3C01;
+    const ARABIC_QATAR: u16 = 0x4001;
+    const ARMENIAN: u16 = 0x042B;
+    const ASSAMESE: u16 = 0x044D;
+    const AZERI_LATIN: u16 = 0x042C;
+    const AZERI_CYRILLIC: u16 = 0x082C;
+    const BASQUE: u16 = 0x042D;
+    const BELARUSSIAN: u16 = 0x0423;
+    const BENGALI: u16 = 0x0445;
+    const BULGARIAN: u16 = 0x0402;
+    const BURMESE: u16 = 0x0455;
+    const CATALAN: u16 = 0x0403;
+    const CHINESE_TAIWAN: u16 = 0x0404;
+    const CHINESE_CHINA: u16 = 0x0804;
+    const CHINESE_HONG_KONG: u16 = 0x0C04;
+    const CHINESE_SINGAPORE: u16 = 0x1004;
+    const CHINESE_MACAU: u16 = 0x1404;
+    const CROATIAN: u16 = 0x041A;
+    const CZECH: u16 = 0x0405;
+    const DANISH: u16 = 0x0406;
+    const DUTCH_NETHERLANDS: u16 = 0x0413;
+    const DUTCH_BELGIUM: u16 = 0x0813;
+    const ENGLISH_UNITED_STATES: u16 = 0x0409;
+    const ENGLISH_UNITED_KINGDOM: u16 = 0x0809;
+    const ENGLISH_AUSTRALIAN: u16 = 0x0C09;
+    const ENGLISH_CANADIAN: u16 = 0x1009;
+    const ENGLISH_NEW_ZEALAND: u16 = 0x1409;
+    const ENGLISH_IRELAND: u16 = 0x1809;
+    const ENGLISH_SOUTH_AFRICA: u16 = 0x1C09;
+    const ENGLISH_JAMAICA: u16 = 0x2009;
+    const ENGLISH_CARIBBEAN: u16 = 0x2409;
+    const ENGLISH_BELIZE: u16 = 0x2809;
+    const ENGLISH_TRINIDAD: u16 = 0x2C09;
+    const ENGLISH_ZIMBABWE: u16 = 0x3009;
+    const ENGLISH_PHILIPPINES: u16 = 0x3409;
+    const ESTONIAN: u16 = 0x0425;
+    const FAEROESE: u16 = 0x0438;
+    const FARSI: u16 = 0x0429;
+    const FINNISH: u16 = 0x040B;
+    const FRENCH_STANDARD: u16 = 0x040C;
+    const FRENCH_BELGIAN: u16 = 0x080C;
+    const FRENCH_CANADIAN: u16 = 0x0C0C;
+    const FRENCH_SWITZERLAND: u16 = 0x100C;
+    const FRENCH_LUXEMBOURG: u16 = 0x140C;
+    const FRENCH_MONACO: u16 = 0x180C;
+    const GEORGIAN: u16 = 0x0437;
+    const GERMAN_STANDARD: u16 = 0x0407;
+    const GERMAN_SWITZERLAND: u16 = 0x0807;
+    const GERMAN_AUSTRIA: u16 = 0x0C07;
+    const GERMAN_LUXEMBOURG: u16 = 0x1007;
+    const GERMAN_LIECHTENSTEIN: u16 = 0x1407;
+    const GREEK: u16 = 0x0408;
+    const GUJARATI: u16 = 0x0447;
+    const HEBREW: u16 = 0x040D;
+    const HINDI: u16 = 0x0439;
+    const HUNGARIAN: u16 = 0x040E;
+    const ICELANDIC: u16 = 0x040F;
+    const INDONESIAN: u16 = 0x0421;
+    const ITALIAN_STANDARD: u16 = 0x0410;
+    const ITALIAN_SWITZERLAND: u16 = 0x0810;
+    const JAPANESE: u16 = 0x0411;
+    const KANNADA: u16 = 0x044B;
+    const KASHMIRI_INDIA: u16 = 0x0860;
+    const KAZAKH: u16 = 0x043F;
+    const KONKANI: u16 = 0x0457;
+    const KOREAN: u16 = 0x0412;
+    const KOREAN_JOHAB: u16 = 0x0812;
+    const LATVIAN: u16 = 0x0426;
+    const LITHUANIAN: u16 = 0x0427;
+    const LITHUANIAN_CLASSIC: u16 = 0x0827;
+    const MACEDONIAN: u16 = 0x042F;
+    const MALAY_MALAYSIAN: u16 = 0x043E;
+    const MALAY_BRUNEI_DARUSSALAM: u16 = 0x083E;
+    const MALAYALAM: u16 = 0x044C;
+    const MANIPURI: u16 = 0x0458;
+    const MARATHI: u16 = 0x044E;
+    const NEPALI_INDIA: u16 = 0x0861;
+    const NORWEGIAN_BOKMAL: u16 = 0x0414;
+    const NORWEGIAN_NYNORSK: u16 = 0x0814;
+    const ORIYA: u16 = 0x0448;
+    const POLISH: u16 = 0x0415;
+    const PORTUGUESE_BRAZIL: u16 = 0x0416;
+    const PORTUGUESE_STANDARD: u16 = 0x0816;
+    const PUNJABI: u16 = 0x0446;
+    const ROMANIAN: u16 = 0x0418;
+    const RUSSIAN: u16 = 0x0419;
+    const SANSKRIT: u16 = 0x044F;
+    const SERBIAN_CYRILLIC: u16 = 0x0C1A;
+    const SERBIAN_LATIN: u16 = 0x081A;
+    const SINDHI: u16 = 0x0459;
+    const SLOVAK: u16 = 0x041B;
+    const SLOVENIAN: u16 = 0x0424;
+    const SPANISH_TRADITIONAL_SORT: u16 = 0x040A;
+    const SPANISH_MEXICAN: u16 = 0x080A;
+    const SPANISH_MODERN_SORT: u16 = 0x0C0A;
+    const SPANISH_GUATEMALA: u16 = 0x100A;
+    const SPANISH_COSTA_RICA: u16 = 0x140A;
+    const SPANISH_PANAMA: u16 = 0x180A;
+    const SPANISH_DOMINICAN_REPUBLIC: u16 = 0x1C0A;
+    const SPANISH_VENEZUELA: u16 = 0x200A;
+    const SPANISH_COLOMBIA: u16 = 0x240A;
+    const SPANISH_PERU: u16 = 0x280A;
+    const SPANISH_ARGENTINA: u16 = 0x2C0A;
+    const SPANISH_ECUADOR: u16 = 0x300A;
+    const SPANISH_CHILE: u16 = 0x340A;
+    const SPANISH_URUGUAY: u16 = 0x380A;
+    const SPANISH_PARAGUAY: u16 = 0x3C0A;
+    const SPANISH_BOLIVIA: u16 = 0x400A;
+    const SPANISH_EL_SALVADOR: u16 = 0x440A;
+    const SPANISH_HONDURAS: u16 = 0x480A;
+    const SPANISH_NICARAGUA: u16 = 0x4C0A;
+    const SPANISH_PUERTO_RICO: u16 = 0x500A;
+    const SUTU: u16 = 0x0430;
+    const SWAHILI_KENYA: u16 = 0x0441;
+    const SWEDISH: u16 = 0x041D;
+    const SWEDISH_FINLAND: u16 = 0x081D;
+    const TAMIL: u16 = 0x0449;
+    const TATAR_TATARSTAN: u16 = 0x0444;
+    const TELUGU: u16 = 0x044A;
+    const THAI: u16 = 0x041E;
+    const TURKISH: u16 = 0x041F;
+    const UKRAINIAN: u16 = 0x0422;
+    const URDU_PAKISTAN: u16 = 0x0420;
+    const URDU_INDIA: u16 = 0x0820;
+    const UZBEK_LATIN: u16 = 0x0443;
+    const UZBEK_CYRILLIC: u16 = 0x0843;
+    const VIETNAMESE: u16 = 0x042A;
+    const HID_USAGE_DATA_DESCRIPTOR: u16 = 0x04FF;
+    const HID_VENDOR_DEFINED_1: u16 = 0xF0FF;
+    const HID_VENDOR_DEFINED_2: u16 = 0xF4FF;
+    const HID_VENDOR_DEFINED_3: u16 = 0xF8FF;
+    const HID_VENDOR_DEFINED_4: u16 = 0xFCFF;
+
+    #[test]
+    fn it_recognizes_afrikaans_as_afrikaans_language() {
+        assert_eq!(
+            super::from_lang_id(AFRIKAANS).primary_language(),
+            PrimaryLanguage::Afrikaans
+        );
+    }
+
+    #[test]
+    fn it_recognizes_albanian_as_albanian_language() {
+        assert_eq!(
+            super::from_lang_id(ALBANIAN).primary_language(),
+            PrimaryLanguage::Albanian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_saudi_arabia_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_SAUDI_ARABIA).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_saudi_arabia_as_saudi_arabia_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_SAUDI_ARABIA).sub_language(),
+            SubLanguage::SaudiArabia
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_iraq_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_IRAQ).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_iraq_as_iraq_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_IRAQ).sub_language(),
+            SubLanguage::Iraq
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_egypt_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_EGYPT).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_egypt_as_egypt_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_EGYPT).sub_language(),
+            SubLanguage::Egypt
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_libya_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_LIBYA).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_libya_as_libya_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_LIBYA).sub_language(),
+            SubLanguage::Libya
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_algeria_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_ALGERIA).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_algeria_as_algeria_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_ALGERIA).sub_language(),
+            SubLanguage::Algeria
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_morocco_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_MOROCCO).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_morocco_as_morocco_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_MOROCCO).sub_language(),
+            SubLanguage::Morocco
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_tunisia_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_TUNISIA).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_tunisia_as_tunisia_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_TUNISIA).sub_language(),
+            SubLanguage::Tunisia
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_oman_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_OMAN).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_oman_as_oman_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_OMAN).sub_language(),
+            SubLanguage::Oman
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_yemen_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_YEMEN).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_yemen_as_yemen_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_YEMEN).sub_language(),
+            SubLanguage::Yemen
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_syria_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_SYRIA).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_syria_as_syria_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_SYRIA).sub_language(),
+            SubLanguage::Syria
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_jordan_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_JORDAN).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_jordan_as_jordan_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_JORDAN).sub_language(),
+            SubLanguage::Jordan
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_lebanon_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_LEBANON).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_lebanon_as_lebanon_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_LEBANON).sub_language(),
+            SubLanguage::Lebanon
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_kuwait_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_KUWAIT).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_kuwait_as_kuwait_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_KUWAIT).sub_language(),
+            SubLanguage::Kuwait
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_uae_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_UAE).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_uae_as_uae_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_UAE).sub_language(),
+            SubLanguage::UnitedArabEmirates
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_bahrain_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_BAHRAIN).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_bahrain_as_bahrain_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_BAHRAIN).sub_language(),
+            SubLanguage::Bahrain
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_qatar_as_arabic_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_QATAR).primary_language(),
+            PrimaryLanguage::Arabic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_arabic_from_qatar_as_qatar_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ARABIC_QATAR).sub_language(),
+            SubLanguage::Qatar
+        );
+    }
+
+    #[test]
+    fn it_recognizes_armenian_as_armenian_language() {
+        assert_eq!(
+            super::from_lang_id(ARMENIAN).primary_language(),
+            PrimaryLanguage::Armenian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_assamese_as_assamese_language() {
+        assert_eq!(
+            super::from_lang_id(ASSAMESE).primary_language(),
+            PrimaryLanguage::Assamese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_azeri_latin_as_azeri_language() {
+        assert_eq!(
+            super::from_lang_id(AZERI_LATIN).primary_language(),
+            PrimaryLanguage::Azeri
+        );
+    }
+
+    #[test]
+    fn it_recognizes_azeri_latin_as_latin_sub_language() {
+        assert_eq!(
+            super::from_lang_id(AZERI_LATIN).sub_language(),
+            SubLanguage::Latin
+        );
+    }
+
+    #[test]
+    fn it_recognizes_azeri_cyrillic_as_azeri_language() {
+        assert_eq!(
+            super::from_lang_id(AZERI_CYRILLIC).primary_language(),
+            PrimaryLanguage::Azeri
+        );
+    }
+
+    #[test]
+    fn it_recognizes_azeri_cyrillic_as_cyrillic_sub_language() {
+        assert_eq!(
+            super::from_lang_id(AZERI_CYRILLIC).sub_language(),
+            SubLanguage::Cyrillic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_basque_as_basque_language() {
+        assert_eq!(
+            super::from_lang_id(BASQUE).primary_language(),
+            PrimaryLanguage::Basque
+        );
+    }
+
+    #[test]
+    fn it_recognizes_belarussian_as_belarussian_language() {
+        assert_eq!(
+            super::from_lang_id(BELARUSSIAN).primary_language(),
+            PrimaryLanguage::Belarussian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_bengali_as_bengali_language() {
+        assert_eq!(
+            super::from_lang_id(BENGALI).primary_language(),
+            PrimaryLanguage::Bengali
+        );
+    }
+
+    #[test]
+    fn it_recognizes_bulgarian_as_bulgarian_language() {
+        assert_eq!(
+            super::from_lang_id(BULGARIAN).primary_language(),
+            PrimaryLanguage::Bulgarian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_burmese_as_burmese_language() {
+        assert_eq!(
+            super::from_lang_id(BURMESE).primary_language(),
+            PrimaryLanguage::Burmese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_catalan_as_catalan_language() {
+        assert_eq!(
+            super::from_lang_id(CATALAN).primary_language(),
+            PrimaryLanguage::Catalan
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_taiwan_as_chinese_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_TAIWAN).primary_language(),
+            PrimaryLanguage::Chinese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_taiwan_as_taiwan_sub_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_TAIWAN).sub_language(),
+            SubLanguage::Taiwan
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_china_as_chinese_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_CHINA).primary_language(),
+            PrimaryLanguage::Chinese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_china_as_china_sub_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_CHINA).sub_language(),
+            SubLanguage::China
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_hong_kong_as_chinese_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_HONG_KONG).primary_language(),
+            PrimaryLanguage::Chinese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_hong_kong_as_hong_kong_sub_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_HONG_KONG).sub_language(),
+            SubLanguage::HongKong
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_singapore_as_chinese_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_SINGAPORE).primary_language(),
+            PrimaryLanguage::Chinese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_singapore_as_singapore_sub_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_SINGAPORE).sub_language(),
+            SubLanguage::Singapore
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_macau_as_chinese_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_MACAU).primary_language(),
+            PrimaryLanguage::Chinese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_chinese_from_macau_as_macau_sub_language() {
+        assert_eq!(
+            super::from_lang_id(CHINESE_MACAU).sub_language(),
+            SubLanguage::Macau
+        );
+    }
+
+    #[test]
+    fn it_recognizes_croatian_as_croatian_language() {
+        assert_eq!(
+            super::from_lang_id(CROATIAN).primary_language(),
+            PrimaryLanguage::Croatian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_czech_as_czech_language() {
+        assert_eq!(
+            super::from_lang_id(CZECH).primary_language(),
+            PrimaryLanguage::Czech
+        );
+    }
+
+    #[test]
+    fn it_recognizes_danish_as_danish_language() {
+        assert_eq!(
+            super::from_lang_id(DANISH).primary_language(),
+            PrimaryLanguage::Danish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_dutch_from_netherlands_as_dutch_language() {
+        assert_eq!(
+            super::from_lang_id(DUTCH_NETHERLANDS).primary_language(),
+            PrimaryLanguage::Dutch
+        );
+    }
+
+    #[test]
+    fn it_recognizes_dutch_from_netherlands_as_netherlands_sub_language() {
+        assert_eq!(
+            super::from_lang_id(DUTCH_NETHERLANDS).sub_language(),
+            SubLanguage::Netherlands
+        );
+    }
+
+    #[test]
+    fn it_recognizes_dutch_from_belgium_as_dutch_language() {
+        assert_eq!(
+            super::from_lang_id(DUTCH_BELGIUM).primary_language(),
+            PrimaryLanguage::Dutch
+        );
+    }
+
+    #[test]
+    fn it_recognizes_dutch_from_belgium_as_belgium_sub_language() {
+        assert_eq!(
+            super::from_lang_id(DUTCH_BELGIUM).sub_language(),
+            SubLanguage::Belgium
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_united_states_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_UNITED_STATES).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_united_states_as_united_states_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_UNITED_STATES).sub_language(),
+            SubLanguage::UnitedStates
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_united_kingdom_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_UNITED_KINGDOM).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_united_kingdom_as_united_kingdom_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_UNITED_KINGDOM).sub_language(),
+            SubLanguage::UnitedKingdom
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_australia_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_AUSTRALIAN).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_australia_as_australia_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_AUSTRALIAN).sub_language(),
+            SubLanguage::Australia
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_canada_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_CANADIAN).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_canada_as_canada_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_CANADIAN).sub_language(),
+            SubLanguage::Canada
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_new_zealand_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_NEW_ZEALAND).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_new_zealand_as_new_zealand_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_NEW_ZEALAND).sub_language(),
+            SubLanguage::NewZealand
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_ireland_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_IRELAND).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_ireland_as_ireland_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_IRELAND).sub_language(),
+            SubLanguage::Ireland
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_south_africa_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_SOUTH_AFRICA).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_south_africa_as_south_africa_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_SOUTH_AFRICA).sub_language(),
+            SubLanguage::SouthAfrica
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_jamaica_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_JAMAICA).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_jamaica_as_jamaica_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_JAMAICA).sub_language(),
+            SubLanguage::Jamaica
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_caribbean_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_CARIBBEAN).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_caribbean_as_caribbean_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_CARIBBEAN).sub_language(),
+            SubLanguage::Caribbean
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_belize_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_BELIZE).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_belize_as_belize_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_BELIZE).sub_language(),
+            SubLanguage::Belize
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_trinidad_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_TRINIDAD).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_trinidad_as_trinidad_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_TRINIDAD).sub_language(),
+            SubLanguage::Trinidad
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_zimbabwe_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_ZIMBABWE).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_zimbabwe_as_zimbabwe_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_ZIMBABWE).sub_language(),
+            SubLanguage::Zimbabwe
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_philippines_as_english_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_PHILIPPINES).primary_language(),
+            PrimaryLanguage::English
+        );
+    }
+
+    #[test]
+    fn it_recognizes_english_from_philippines_as_philippines_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ENGLISH_PHILIPPINES).sub_language(),
+            SubLanguage::Philippines
+        );
+    }
+
+    #[test]
+    fn it_recognizes_estonian_as_estonian_language() {
+        assert_eq!(
+            super::from_lang_id(ESTONIAN).primary_language(),
+            PrimaryLanguage::Estonian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_faeroese_as_faeroese_language() {
+        assert_eq!(
+            super::from_lang_id(FAEROESE).primary_language(),
+            PrimaryLanguage::Faeroese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_farsi_as_farsi_language() {
+        assert_eq!(
+            super::from_lang_id(FARSI).primary_language(),
+            PrimaryLanguage::Farsi
+        );
+    }
+
+    #[test]
+    fn it_recognizes_finnish_as_finnish_language() {
+        assert_eq!(
+            super::from_lang_id(FINNISH).primary_language(),
+            PrimaryLanguage::Finnish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_standard_as_french_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_STANDARD).primary_language(),
+            PrimaryLanguage::French
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_standard_as_standard_sub_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_STANDARD).sub_language(),
+            SubLanguage::Standard
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_belgium_as_french_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_BELGIAN).primary_language(),
+            PrimaryLanguage::French
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_belgium_as_belgium_sub_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_BELGIAN).sub_language(),
+            SubLanguage::Belgium
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_canada_as_french_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_CANADIAN).primary_language(),
+            PrimaryLanguage::French
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_canada_as_canada_sub_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_CANADIAN).sub_language(),
+            SubLanguage::Canada
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_switzerland_as_french_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_SWITZERLAND).primary_language(),
+            PrimaryLanguage::French
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_switzerland_as_switzerland_sub_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_SWITZERLAND).sub_language(),
+            SubLanguage::Switzerland
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_luxembourg_as_french_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_LUXEMBOURG).primary_language(),
+            PrimaryLanguage::French
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_luxembourg_as_luxembourg_sub_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_LUXEMBOURG).sub_language(),
+            SubLanguage::Luxembourg
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_monaco_as_french_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_MONACO).primary_language(),
+            PrimaryLanguage::French
+        );
+    }
+
+    #[test]
+    fn it_recognizes_french_from_monaco_as_monaco_sub_language() {
+        assert_eq!(
+            super::from_lang_id(FRENCH_MONACO).sub_language(),
+            SubLanguage::Monaco
+        );
+    }
+
+    #[test]
+    fn it_recognizes_georgian_as_georgian_language() {
+        assert_eq!(
+            super::from_lang_id(GEORGIAN).primary_language(),
+            PrimaryLanguage::Georgian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_standard_as_german_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_STANDARD).primary_language(),
+            PrimaryLanguage::German
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_standard_as_standard_sub_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_STANDARD).sub_language(),
+            SubLanguage::Standard
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_switzerland_as_german_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_SWITZERLAND).primary_language(),
+            PrimaryLanguage::German
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_switzerland_as_switzerland_sub_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_SWITZERLAND).sub_language(),
+            SubLanguage::Switzerland
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_austria_as_german_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_AUSTRIA).primary_language(),
+            PrimaryLanguage::German
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_austria_as_austria_sub_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_AUSTRIA).sub_language(),
+            SubLanguage::Austria
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_luxembourg_as_german_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_LUXEMBOURG).primary_language(),
+            PrimaryLanguage::German
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_luxembourg_as_luxembourg_sub_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_LUXEMBOURG).sub_language(),
+            SubLanguage::Luxembourg
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_liechtenstein_as_german_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_LIECHTENSTEIN).primary_language(),
+            PrimaryLanguage::German
+        );
+    }
+
+    #[test]
+    fn it_recognizes_german_from_liechtenstein_as_liechtenstein_sub_language() {
+        assert_eq!(
+            super::from_lang_id(GERMAN_LIECHTENSTEIN).sub_language(),
+            SubLanguage::Liechtenstein
+        );
+    }
+
+    #[test]
+    fn it_recognizes_greek_as_greek_language() {
+        assert_eq!(
+            super::from_lang_id(GREEK).primary_language(),
+            PrimaryLanguage::Greek
+        );
+    }
+
+    #[test]
+    fn it_recognizes_gujarati_as_gujarati_language() {
+        assert_eq!(
+            super::from_lang_id(GUJARATI).primary_language(),
+            PrimaryLanguage::Gujarati
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hebrew_as_hebrew_language() {
+        assert_eq!(
+            super::from_lang_id(HEBREW).primary_language(),
+            PrimaryLanguage::Hebrew
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hindi_as_hindi_language() {
+        assert_eq!(
+            super::from_lang_id(HINDI).primary_language(),
+            PrimaryLanguage::Hindi
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hungarian_as_hungarian_language() {
+        assert_eq!(
+            super::from_lang_id(HUNGARIAN).primary_language(),
+            PrimaryLanguage::Hungarian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_icelandic_as_icelandic_language() {
+        assert_eq!(
+            super::from_lang_id(ICELANDIC).primary_language(),
+            PrimaryLanguage::Icelandic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_indonesian_as_indonesian_language() {
+        assert_eq!(
+            super::from_lang_id(INDONESIAN).primary_language(),
+            PrimaryLanguage::Indonesian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_italian_standard_as_italian_language() {
+        assert_eq!(
+            super::from_lang_id(ITALIAN_STANDARD).primary_language(),
+            PrimaryLanguage::Italian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_italian_standard_as_standard_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ITALIAN_STANDARD).sub_language(),
+            SubLanguage::Standard
+        );
+    }
+
+    #[test]
+    fn it_recognizes_italian_from_switzerland_as_italian_language() {
+        assert_eq!(
+            super::from_lang_id(ITALIAN_SWITZERLAND).primary_language(),
+            PrimaryLanguage::Italian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_italian_from_switzerland_as_switzerland_sub_language() {
+        assert_eq!(
+            super::from_lang_id(ITALIAN_SWITZERLAND).sub_language(),
+            SubLanguage::Switzerland
+        );
+    }
+
+    #[test]
+    fn it_recognizes_japanese_as_japanese_language() {
+        assert_eq!(
+            super::from_lang_id(JAPANESE).primary_language(),
+            PrimaryLanguage::Japanese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_kannada_as_kannada_language() {
+        assert_eq!(
+            super::from_lang_id(KANNADA).primary_language(),
+            PrimaryLanguage::Kannada
+        );
+    }
+
+    #[test]
+    fn it_recognizes_kashmiri_as_kashmiri_language() {
+        assert_eq!(
+            super::from_lang_id(KASHMIRI_INDIA).primary_language(),
+            PrimaryLanguage::Kashmiri
+        );
+    }
+
+    #[test]
+    fn it_recognizes_kazakh_as_kazakh_language() {
+        assert_eq!(
+            super::from_lang_id(KAZAKH).primary_language(),
+            PrimaryLanguage::Kazakh
+        );
+    }
+
+    #[test]
+    fn it_recognizes_konkani_as_konkani_language() {
+        assert_eq!(
+            super::from_lang_id(KONKANI).primary_language(),
+            PrimaryLanguage::Konkani
+        );
+    }
+
+    #[test]
+    fn it_recognizes_korean_as_korean_language() {
+        assert_eq!(
+            super::from_lang_id(KOREAN).primary_language(),
+            PrimaryLanguage::Korean
+        );
+    }
+
+    #[test]
+    fn it_recognizes_korean_as_standard_sub_language() {
+        assert_eq!(
+            super::from_lang_id(KOREAN).sub_language(),
+            SubLanguage::Standard
+        );
+    }
+
+    #[test]
+    fn it_recognizes_korean_johab_as_korean_language() {
+        assert_eq!(
+            super::from_lang_id(KOREAN_JOHAB).primary_language(),
+            PrimaryLanguage::Korean
+        );
+    }
+
+    #[test]
+    fn it_recognizes_korean_johab_as_johab_sub_language() {
+        assert_eq!(
+            super::from_lang_id(KOREAN_JOHAB).sub_language(),
+            SubLanguage::Johab
+        );
+    }
+
+    #[test]
+    fn it_recognizes_latvian_as_latvian_language() {
+        assert_eq!(
+            super::from_lang_id(LATVIAN).primary_language(),
+            PrimaryLanguage::Latvian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_lithuanian_as_lithuanian_language() {
+        assert_eq!(
+            super::from_lang_id(LITHUANIAN).primary_language(),
+            PrimaryLanguage::Lithuanian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_lithuanian_as_standard_sub_language() {
+        assert_eq!(
+            super::from_lang_id(LITHUANIAN).sub_language(),
+            SubLanguage::Standard
+        );
+    }
+
+    #[test]
+    fn it_recognizes_lithuanian_classic_as_lithuanian_language() {
+        assert_eq!(
+            super::from_lang_id(LITHUANIAN_CLASSIC).primary_language(),
+            PrimaryLanguage::Lithuanian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_lithuanian_classic_as_classic_sub_language() {
+        assert_eq!(
+            super::from_lang_id(LITHUANIAN_CLASSIC).sub_language(),
+            SubLanguage::Classic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_macedonian_as_macedonian_language() {
+        assert_eq!(
+            super::from_lang_id(MACEDONIAN).primary_language(),
+            PrimaryLanguage::Macedonian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_malay_from_malaysia_as_malay_language() {
+        assert_eq!(
+            super::from_lang_id(MALAY_MALAYSIAN).primary_language(),
+            PrimaryLanguage::Malay
+        );
+    }
+
+    #[test]
+    fn it_recognizes_malay_from_malaysia_as_malaysia_sub_language() {
+        assert_eq!(
+            super::from_lang_id(MALAY_MALAYSIAN).sub_language(),
+            SubLanguage::Malaysia
+        );
+    }
+
+    #[test]
+    fn it_recognizes_malay_from_brunei_darussalam_as_malay_language() {
+        assert_eq!(
+            super::from_lang_id(MALAY_BRUNEI_DARUSSALAM).primary_language(),
+            PrimaryLanguage::Malay
+        );
+    }
+
+    #[test]
+    fn it_recognizes_malay_from_brunei_darussalam_as_brunei_darussalam_sub_language() {
+        assert_eq!(
+            super::from_lang_id(MALAY_BRUNEI_DARUSSALAM).sub_language(),
+            SubLanguage::BruneiDarussalam
+        );
+    }
+
+    #[test]
+    fn it_recognizes_malayalam_as_malayalam_language() {
+        assert_eq!(
+            super::from_lang_id(MALAYALAM).primary_language(),
+            PrimaryLanguage::Malayalam
+        );
+    }
+
+    #[test]
+    fn it_recognizes_manipuri_as_manipuri_language() {
+        assert_eq!(
+            super::from_lang_id(MANIPURI).primary_language(),
+            PrimaryLanguage::Manipuri
+        );
+    }
+
+    #[test]
+    fn it_recognizes_marathi_as_marathi_language() {
+        assert_eq!(
+            super::from_lang_id(MARATHI).primary_language(),
+            PrimaryLanguage::Marathi
+        );
+    }
+
+    #[test]
+    fn it_recognizes_nepali_as_nepali_language() {
+        assert_eq!(
+            super::from_lang_id(NEPALI_INDIA).primary_language(),
+            PrimaryLanguage::Nepali
+        );
+    }
+
+    #[test]
+    fn it_recognizes_norwegian_bokmal_as_norwegian_language() {
+        assert_eq!(
+            super::from_lang_id(NORWEGIAN_BOKMAL).primary_language(),
+            PrimaryLanguage::Norwegian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_norwegian_bokmal_as_bokmal_sub_language() {
+        assert_eq!(
+            super::from_lang_id(NORWEGIAN_BOKMAL).sub_language(),
+            SubLanguage::Bokmal
+        );
+    }
+
+    #[test]
+    fn it_recognizes_norwegian_nynorsk_as_norwegian_language() {
+        assert_eq!(
+            super::from_lang_id(NORWEGIAN_NYNORSK).primary_language(),
+            PrimaryLanguage::Norwegian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_norwegian_nynorsk_as_nynorsk_sub_language() {
+        assert_eq!(
+            super::from_lang_id(NORWEGIAN_NYNORSK).sub_language(),
+            SubLanguage::Nynorsk
+        );
+    }
+
+    #[test]
+    fn it_recognizes_oriya_as_oriya_language() {
+        assert_eq!(
+            super::from_lang_id(ORIYA).primary_language(),
+            PrimaryLanguage::Oriya
+        );
+    }
+
+    #[test]
+    fn it_recognizes_polish_as_polish_language() {
+        assert_eq!(
+            super::from_lang_id(POLISH).primary_language(),
+            PrimaryLanguage::Polish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_portuguese_from_brazil_as_portuguese_language() {
+        assert_eq!(
+            super::from_lang_id(PORTUGUESE_BRAZIL).primary_language(),
+            PrimaryLanguage::Portuguese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_portuguese_from_brazil_as_brazil_sub_language() {
+        assert_eq!(
+            super::from_lang_id(PORTUGUESE_BRAZIL).sub_language(),
+            SubLanguage::Brazil
+        );
+    }
+
+    #[test]
+    fn it_recognizes_portuguese_standard_as_portuguese_language() {
+        assert_eq!(
+            super::from_lang_id(PORTUGUESE_STANDARD).primary_language(),
+            PrimaryLanguage::Portuguese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_portuguese_standard_as_standard_sub_language() {
+        assert_eq!(
+            super::from_lang_id(PORTUGUESE_STANDARD).sub_language(),
+            SubLanguage::Standard
+        );
+    }
+
+    #[test]
+    fn it_recognizes_punjabi_as_punjabi_language() {
+        assert_eq!(
+            super::from_lang_id(PUNJABI).primary_language(),
+            PrimaryLanguage::Punjabi
+        );
+    }
+
+    #[test]
+    fn it_recognizes_romanian_as_romanian_language() {
+        assert_eq!(
+            super::from_lang_id(ROMANIAN).primary_language(),
+            PrimaryLanguage::Romanian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_russian_as_russian_language() {
+        assert_eq!(
+            super::from_lang_id(RUSSIAN).primary_language(),
+            PrimaryLanguage::Russian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_sanskrit_as_sanskrit_language() {
+        assert_eq!(
+            super::from_lang_id(SANSKRIT).primary_language(),
+            PrimaryLanguage::Sanskrit
+        );
+    }
+
+    #[test]
+    fn it_recognizes_serbian_cyrillic_as_serbian_language() {
+        assert_eq!(
+            super::from_lang_id(SERBIAN_CYRILLIC).primary_language(),
+            PrimaryLanguage::Serbian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_serbian_cyrillic_as_cyrillic_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SERBIAN_CYRILLIC).sub_language(),
+            SubLanguage::Cyrillic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_serbian_latin_as_serbian_language() {
+        assert_eq!(
+            super::from_lang_id(SERBIAN_LATIN).primary_language(),
+            PrimaryLanguage::Serbian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_serbian_latin_as_latin_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SERBIAN_LATIN).sub_language(),
+            SubLanguage::Latin
+        );
+    }
+
+    #[test]
+    fn it_recognizes_sindhi_as_sindhi_language() {
+        assert_eq!(
+            super::from_lang_id(SINDHI).primary_language(),
+            PrimaryLanguage::Sindhi
+        );
+    }
+
+    #[test]
+    fn it_recognizes_slovak_as_slovak_language() {
+        assert_eq!(
+            super::from_lang_id(SLOVAK).primary_language(),
+            PrimaryLanguage::Slovak
+        );
+    }
+
+    #[test]
+    fn it_recognizes_slovenian_as_slovenian_language() {
+        assert_eq!(
+            super::from_lang_id(SLOVENIAN).primary_language(),
+            PrimaryLanguage::Slovenian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_traditional_sort_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_TRADITIONAL_SORT).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_traditional_sort_as_traditional_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_TRADITIONAL_SORT).sub_language(),
+            SubLanguage::Traditional
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_mexico_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_MEXICAN).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_mexico_as_mexico_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_MEXICAN).sub_language(),
+            SubLanguage::Mexico
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_modern_sort_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_MODERN_SORT).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_modern_sort_as_modern_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_MODERN_SORT).sub_language(),
+            SubLanguage::Modern
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_guatemala_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_GUATEMALA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_guatemala_as_guatemala_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_GUATEMALA).sub_language(),
+            SubLanguage::Guatemala
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_costa_rica_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_COSTA_RICA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_costa_rica_as_costa_rica_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_COSTA_RICA).sub_language(),
+            SubLanguage::CostaRica
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_panama_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PANAMA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_panama_as_panama_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PANAMA).sub_language(),
+            SubLanguage::Panama
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_dominican_republic_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_DOMINICAN_REPUBLIC).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_dominican_republic_as_dominican_republic_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_DOMINICAN_REPUBLIC).sub_language(),
+            SubLanguage::DominicanRepublic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_venezuela_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_VENEZUELA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_venezuela_as_venezuela_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_VENEZUELA).sub_language(),
+            SubLanguage::Venezuela
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_colombia_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_COLOMBIA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_colombia_as_colombia_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_COLOMBIA).sub_language(),
+            SubLanguage::Colombia
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_peru_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PERU).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_peru_as_peru_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PERU).sub_language(),
+            SubLanguage::Peru
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_argentina_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_ARGENTINA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_argentina_as_argentina_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_ARGENTINA).sub_language(),
+            SubLanguage::Argentina
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_ecuador_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_ECUADOR).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_ecuador_as_ecuador_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_ECUADOR).sub_language(),
+            SubLanguage::Ecuador
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_chile_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_CHILE).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_chile_as_chile_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_CHILE).sub_language(),
+            SubLanguage::Chile
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_uruguay_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_URUGUAY).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_uruguay_as_uruguay_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_URUGUAY).sub_language(),
+            SubLanguage::Uruguay
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_paraguay_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PARAGUAY).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_paraguay_as_paraguay_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PARAGUAY).sub_language(),
+            SubLanguage::Paraguay
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_bolivia_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_BOLIVIA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_bolivia_as_bolivia_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_BOLIVIA).sub_language(),
+            SubLanguage::Bolivia
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_el_salvador_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_EL_SALVADOR).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_el_salvador_as_el_salvador_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_EL_SALVADOR).sub_language(),
+            SubLanguage::ElSalvador
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_honduras_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_HONDURAS).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_honduras_as_honduras_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_HONDURAS).sub_language(),
+            SubLanguage::Honduras
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_nicaragua_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_NICARAGUA).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_nicaragua_as_nicaragua_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_NICARAGUA).sub_language(),
+            SubLanguage::Nicaragua
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_puerto_rico_as_spanish_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PUERTO_RICO).primary_language(),
+            PrimaryLanguage::Spanish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_spanish_from_puerto_rico_as_puerto_rico_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SPANISH_PUERTO_RICO).sub_language(),
+            SubLanguage::PuertoRico
+        );
+    }
+
+    #[test]
+    fn it_recognizes_sutu_as_sutu_language() {
+        assert_eq!(
+            super::from_lang_id(SUTU).primary_language(),
+            PrimaryLanguage::Sutu
+        );
+    }
+
+    #[test]
+    fn it_recognizes_swahili_as_swahili_language() {
+        assert_eq!(
+            super::from_lang_id(SWAHILI_KENYA).primary_language(),
+            PrimaryLanguage::Swahili
+        );
+    }
+
+    #[test]
+    fn it_recognizes_swedish_as_swedish_language() {
+        assert_eq!(
+            super::from_lang_id(SWEDISH).primary_language(),
+            PrimaryLanguage::Swedish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_swedish_as_standard_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SWEDISH).sub_language(),
+            SubLanguage::Standard
+        );
+    }
+
+    #[test]
+    fn it_recognizes_swedish_from_finland_as_swedish_language() {
+        assert_eq!(
+            super::from_lang_id(SWEDISH_FINLAND).primary_language(),
+            PrimaryLanguage::Swedish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_swedish_from_finland_as_finland_sub_language() {
+        assert_eq!(
+            super::from_lang_id(SWEDISH_FINLAND).sub_language(),
+            SubLanguage::Finland
+        );
+    }
+
+    #[test]
+    fn it_recognizes_tamil_as_tamil_language() {
+        assert_eq!(
+            super::from_lang_id(TAMIL).primary_language(),
+            PrimaryLanguage::Tamil
+        );
+    }
+
+    #[test]
+    fn it_recognizes_tatar_as_tatar_language() {
+        assert_eq!(
+            super::from_lang_id(TATAR_TATARSTAN).primary_language(),
+            PrimaryLanguage::Tatar
+        );
+    }
+
+    #[test]
+    fn it_recognizes_telugu_as_telugu_language() {
+        assert_eq!(
+            super::from_lang_id(TELUGU).primary_language(),
+            PrimaryLanguage::Telugu
+        );
+    }
+
+    #[test]
+    fn it_recognizes_thai_as_thai_language() {
+        assert_eq!(
+            super::from_lang_id(THAI).primary_language(),
+            PrimaryLanguage::Thai
+        );
+    }
+
+    #[test]
+    fn it_recognizes_turkish_as_turkish_language() {
+        assert_eq!(
+            super::from_lang_id(TURKISH).primary_language(),
+            PrimaryLanguage::Turkish
+        );
+    }
+
+    #[test]
+    fn it_recognizes_ukrainian_as_ukrainian_language() {
+        assert_eq!(
+            super::from_lang_id(UKRAINIAN).primary_language(),
+            PrimaryLanguage::Ukrainian
+        );
+    }
+
+    #[test]
+    fn it_recognizes_urdu_from_pakistan_as_urdu_language() {
+        assert_eq!(
+            super::from_lang_id(URDU_PAKISTAN).primary_language(),
+            PrimaryLanguage::Urdu
+        );
+    }
+
+    #[test]
+    fn it_recognizes_urdu_from_pakistan_as_pakistan_sub_language() {
+        assert_eq!(
+            super::from_lang_id(URDU_PAKISTAN).sub_language(),
+            SubLanguage::Pakistan
+        );
+    }
+
+    #[test]
+    fn it_recognizes_urdu_from_india_as_urdu_language() {
+        assert_eq!(
+            super::from_lang_id(URDU_INDIA).primary_language(),
+            PrimaryLanguage::Urdu
+        );
+    }
+
+    #[test]
+    fn it_recognizes_urdu_from_india_as_india_sub_language() {
+        assert_eq!(
+            super::from_lang_id(URDU_INDIA).sub_language(),
+            SubLanguage::India
+        );
+    }
+
+    #[test]
+    fn it_recognizes_uzbek_latin_as_uzbek_language() {
+        assert_eq!(
+            super::from_lang_id(UZBEK_LATIN).primary_language(),
+            PrimaryLanguage::Uzbek
+        );
+    }
+
+    #[test]
+    fn it_recognizes_uzbek_latin_as_latin_sub_language() {
+        assert_eq!(
+            super::from_lang_id(UZBEK_LATIN).sub_language(),
+            SubLanguage::Latin
+        );
+    }
+
+    #[test]
+    fn it_recognizes_uzbek_cyrillic_as_uzbek_language() {
+        assert_eq!(
+            super::from_lang_id(UZBEK_CYRILLIC).primary_language(),
+            PrimaryLanguage::Uzbek
+        );
+    }
+
+    #[test]
+    fn it_recognizes_uzbek_cyrillic_as_cyrillic_sub_language() {
+        assert_eq!(
+            super::from_lang_id(UZBEK_CYRILLIC).sub_language(),
+            SubLanguage::Cyrillic
+        );
+    }
+
+    #[test]
+    fn it_recognizes_vietnamese_as_vietnamese_language() {
+        assert_eq!(
+            super::from_lang_id(VIETNAMESE).primary_language(),
+            PrimaryLanguage::Vietnamese
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_usage_data_descriptor_as_hid_language() {
+        assert_eq!(
+            super::from_lang_id(HID_USAGE_DATA_DESCRIPTOR).primary_language(),
+            PrimaryLanguage::HID
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_usage_data_descriptor_as_usage_data_descriptor_sub_language() {
+        assert_eq!(
+            super::from_lang_id(HID_USAGE_DATA_DESCRIPTOR).sub_language(),
+            SubLanguage::UsageDataDescriptor
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_1_as_hid_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_1).primary_language(),
+            PrimaryLanguage::HID
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_1_as_vendor_defined_1_sub_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_1).sub_language(),
+            SubLanguage::VendorDefined1
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_2_as_hid_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_2).primary_language(),
+            PrimaryLanguage::HID
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_1_as_vendor_defined_2_sub_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_2).sub_language(),
+            SubLanguage::VendorDefined2
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_3_as_hid_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_3).primary_language(),
+            PrimaryLanguage::HID
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_1_as_vendor_defined_3_sub_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_3).sub_language(),
+            SubLanguage::VendorDefined3
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_4_as_hid_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_4).primary_language(),
+            PrimaryLanguage::HID
+        );
+    }
+
+    #[test]
+    fn it_recognizes_hid_vendor_defined_1_as_vendor_defined_4_sub_language() {
+        assert_eq!(
+            super::from_lang_id(HID_VENDOR_DEFINED_4).sub_language(),
+            SubLanguage::VendorDefined4
+        );
+    }
+
+    #[test]
+    fn it_recognizes_other_as_other_language() {
+        assert_eq!(
+            super::from_lang_id(0xFFFF).primary_language(),
+            PrimaryLanguage::Other(PRIMARY_LANGUAGE_MASK)
+        );
+    }
+
+    #[test]
+    fn it_recognizes_other_as_other_sub_language() {
+        assert_eq!(
+            super::from_lang_id(0xFFFF).sub_language(),
+            SubLanguage::Other(SUB_LANGUAGE_MASK)
+        );
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..4c16277
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,121 @@
+//! This crate provides a safe wrapper around the native `libusb` library.
+
+pub use libusb1_sys as ffi;
+pub use libusb1_sys::constants;
+
+#[cfg(unix)]
+pub use crate::options::disable_device_discovery;
+pub use crate::{
+    config_descriptor::{ConfigDescriptor, Interfaces},
+    context::{Context, GlobalContext, LogCallbackMode, LogLevel, UsbContext},
+    device::Device,
+    device_descriptor::DeviceDescriptor,
+    device_handle::DeviceHandle,
+    device_list::{DeviceList, Devices},
+    endpoint_descriptor::EndpointDescriptor,
+    error::{Error, Result},
+    fields::{
+        request_type, Direction, Recipient, RequestType, Speed, SyncType, TransferType, UsageType,
+        Version,
+    },
+    hotplug::{Hotplug, HotplugBuilder, Registration},
+    interface_descriptor::{
+        EndpointDescriptors, Interface, InterfaceDescriptor, InterfaceDescriptors,
+    },
+    language::{Language, PrimaryLanguage, SubLanguage},
+    options::UsbOption,
+    version::{version, LibraryVersion},
+};
+
+#[cfg(test)]
+#[macro_use]
+mod test_helpers;
+
+#[macro_use]
+mod error;
+mod version;
+
+mod context;
+mod device;
+mod device_handle;
+mod device_list;
+
+mod config_descriptor;
+mod device_descriptor;
+mod endpoint_descriptor;
+mod fields;
+mod hotplug;
+mod interface_descriptor;
+mod language;
+mod options;
+
+/// Tests whether the running `libusb` library supports capability API.
+pub fn has_capability() -> bool {
+    GlobalContext::default().as_raw();
+    unsafe { libusb1_sys::libusb_has_capability(constants::LIBUSB_CAP_HAS_CAPABILITY) != 0 }
+}
+
+/// Tests whether the running `libusb` library supports hotplug.
+pub fn has_hotplug() -> bool {
+    GlobalContext::default().as_raw();
+    unsafe { libusb1_sys::libusb_has_capability(constants::LIBUSB_CAP_HAS_HOTPLUG) != 0 }
+}
+
+/// Tests whether the running `libusb` library has HID access.
+pub fn has_hid_access() -> bool {
+    GlobalContext::default().as_raw();
+    unsafe { libusb1_sys::libusb_has_capability(constants::LIBUSB_CAP_HAS_HID_ACCESS) != 0 }
+}
+
+/// Tests whether the running `libusb` library supports detaching the kernel driver.
+pub fn supports_detach_kernel_driver() -> bool {
+    GlobalContext::default().as_raw();
+    unsafe {
+        libusb1_sys::libusb_has_capability(constants::LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER) != 0
+    }
+}
+
+/// Returns a list of the current USB devices. Using global context
+pub fn devices() -> crate::Result<DeviceList<GlobalContext>> {
+    GlobalContext::default().devices()
+}
+
+/// Sets the log level of a `libusb` global context.
+pub fn set_log_level(level: LogLevel) {
+    unsafe {
+        libusb1_sys::libusb_set_debug(GlobalContext::default().as_raw(), level.as_c_int());
+    }
+}
+
+/// Convenience function to open a device by its vendor ID and product ID.
+/// Using global context
+///
+/// This function is provided as a convenience for building prototypes without having to
+/// iterate a [`DeviceList`](struct.DeviceList.html). It is not meant for production
+/// applications.
+///
+/// Returns a device handle for the first device found matching `vendor_id` and `product_id`.
+/// On error, or if the device could not be found, it returns `None`.
+pub fn open_device_with_vid_pid(
+    vendor_id: u16,
+    product_id: u16,
+) -> Option<DeviceHandle<GlobalContext>> {
+    let handle = unsafe {
+        libusb1_sys::libusb_open_device_with_vid_pid(
+            GlobalContext::default().as_raw(),
+            vendor_id,
+            product_id,
+        )
+    };
+
+    if handle.is_null() {
+        None
+    } else {
+        Some(unsafe {
+            DeviceHandle::from_libusb(
+                GlobalContext::default(),
+                std::ptr::NonNull::new_unchecked(handle),
+            )
+        })
+    }
+}
diff --git a/src/options.rs b/src/options.rs
new file mode 100644
index 0000000..c527bd8
--- /dev/null
+++ b/src/options.rs
@@ -0,0 +1,57 @@
+use crate::{error, UsbContext};
+use libusb1_sys::{constants::*, libusb_set_option};
+
+/// A `libusb` runtime option that can be enabled for a context.
+pub struct UsbOption {
+    inner: OptionInner,
+}
+
+impl UsbOption {
+    /// Use the [UsbDk] backend if available.
+    ///
+    /// **Note**: This method is available on **Windows** only!
+    ///
+    /// [UsbDk]: https://github.com/daynix/UsbDk
+    #[cfg(windows)]
+    pub fn use_usbdk() -> Self {
+        Self {
+            inner: OptionInner::UseUsbdk,
+        }
+    }
+
+    pub(crate) fn apply<T: UsbContext>(&self, ctx: &mut T) -> crate::Result<()> {
+        match self.inner {
+            OptionInner::UseUsbdk => {
+                let err = unsafe { libusb_set_option(ctx.as_raw(), LIBUSB_OPTION_USE_USBDK) };
+                if err == LIBUSB_SUCCESS {
+                    Ok(())
+                } else {
+                    Err(error::from_libusb(err))
+                }
+            }
+        }
+    }
+}
+
+enum OptionInner {
+    #[cfg_attr(not(windows), allow(dead_code))] // only constructed on Windows
+    UseUsbdk,
+}
+
+/// Disable device scanning in `libusb` init.
+///
+/// Hotplug functionality will also be deactivated.
+///
+/// This is a Linux only option and it must be set before any [`Context`]
+/// creation.
+///
+/// The option is useful in combination with [`Context::open_device_with_fd()`],
+/// which can access a device directly without prior device scanning.
+#[cfg(unix)]
+pub fn disable_device_discovery() -> crate::Result<()> {
+    try_unsafe!(libusb1_sys::libusb_set_option(
+        std::ptr::null_mut(),
+        LIBUSB_OPTION_NO_DEVICE_DISCOVERY
+    ));
+    Ok(())
+}
diff --git a/src/test_helpers.rs b/src/test_helpers.rs
new file mode 100644
index 0000000..e981819
--- /dev/null
+++ b/src/test_helpers.rs
@@ -0,0 +1,164 @@
+pub use std::ptr;
+
+macro_rules! merge {
+    ($default:expr => $($field:ident : $value:expr),*) => {
+        {
+            let mut x = $default;
+            $( x.$field = $value; )*
+
+            x
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! endpoint_descriptor {
+    ($($key:ident : $value:expr),*) => {
+        merge!(
+            libusb1_sys::libusb_endpoint_descriptor {
+                bLength:          7,
+                bDescriptorType:  0x05,
+                bEndpointAddress: 0x00,
+                bmAttributes:     0x00,
+                wMaxPacketSize:   16,
+                bInterval:        1,
+                bRefresh:         1,
+                bSynchAddress:    0,
+                extra:            $crate::test_helpers::ptr::null(),
+                extra_length:     0
+            } => $($key: $value),*
+        )
+    }
+}
+
+#[macro_export]
+macro_rules! interface_descriptor {
+    ($($key:ident : $value:expr),*) => {
+        merge!(
+            libusb1_sys::libusb_interface_descriptor {
+                bLength:            9,
+                bDescriptorType:    0x04,
+                bInterfaceNumber:   0,
+                bAlternateSetting:  0,
+                bNumEndpoints:      0,
+                bInterfaceClass:    0,
+                bInterfaceSubClass: 0,
+                bInterfaceProtocol: 0,
+                iInterface:         0,
+                endpoint:           $crate::test_helpers::ptr::null(),
+                extra:              $crate::test_helpers::ptr::null(),
+                extra_length:       0
+            } => $($key: $value),*
+        )
+    };
+    ($($endpoint:expr),+) => {
+        {
+            let endpoints = vec![$($endpoint),+];
+
+            let r = libusb1_sys::libusb_interface_descriptor {
+                bLength:            9,
+                bDescriptorType:    0x04,
+                bInterfaceNumber:   0,
+                bAlternateSetting:  0,
+                bNumEndpoints:      endpoints.len() as u8,
+                bInterfaceClass:    0,
+                bInterfaceSubClass: 0,
+                bInterfaceProtocol: 0,
+                iInterface:         0,
+                endpoint:           (&endpoints[..]).as_ptr(),
+                extra:              $crate::test_helpers::ptr::null(),
+                extra_length:       0
+            };
+
+            // leak the Vec so the returned pointer remains valid
+            ::std::mem::forget(endpoints);
+            r
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! interface {
+    ($($descriptor:expr),*) => {
+        {
+            let descriptors = vec![$($descriptor),*];
+
+            let r = libusb1_sys::libusb_interface {
+                altsetting:     descriptors.as_ptr(),
+                num_altsetting: descriptors.len() as ::libc::c_int
+            };
+
+            // leak the Vec so the returned pointer remains valid
+            ::std::mem::forget(descriptors);
+            r
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! config_descriptor {
+    ($($key:ident : $value:expr),*) => {
+        merge!(
+            libusb1_sys::libusb_config_descriptor {
+                bLength:             9,
+                bDescriptorType:     0x02,
+                wTotalLength:        9,
+                bNumInterfaces:      0,
+                bConfigurationValue: 0,
+                iConfiguration:      0,
+                bmAttributes:        0x00,
+                bMaxPower:           10,
+                interface:           $crate::test_helpers::ptr::null(),
+                extra:               $crate::test_helpers::ptr::null(),
+                extra_length:        0
+            } => $($key: $value),*
+        )
+    };
+    ($($interface:expr),+) => {
+        {
+            let interfaces = vec![$($interface),+];
+
+            let r = libusb1_sys::libusb_config_descriptor {
+                bLength:             9,
+                bDescriptorType:     0x02,
+                wTotalLength:        9,
+                bNumInterfaces:      interfaces.len() as u8,
+                bConfigurationValue: 0,
+                iConfiguration:      0,
+                bmAttributes:        0x00,
+                bMaxPower:           10,
+                interface:           (&interfaces[..]).as_ptr(),
+                extra:               $crate::test_helpers::ptr::null(),
+                extra_length:        0
+            };
+
+            // leak the Vec so the returned pointer remains valid
+            ::std::mem::forget(interfaces);
+            r
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! device_descriptor {
+    ($($key:ident : $value:expr),*) => {
+        merge!(
+            libusb1_sys::libusb_device_descriptor {
+                bLength:            18,
+                bDescriptorType:    0x01,
+                bcdUSB:             0x0110,
+                bDeviceClass:       0,
+                bDeviceSubClass:    0,
+                bDeviceProtocol:    0,
+                bMaxPacketSize0:    16,
+                idVendor:           0x1234,
+                idProduct:          0x5678,
+                bcdDevice:          0x0123,
+                iManufacturer:      0,
+                iProduct:           0,
+                iSerialNumber:      0,
+                bNumConfigurations: 1
+            } => $($key: $value),*
+        )
+    }
+}
diff --git a/src/version.rs b/src/version.rs
new file mode 100644
index 0000000..fb624e6
--- /dev/null
+++ b/src/version.rs
@@ -0,0 +1,67 @@
+use std::{ffi::CStr, fmt, str};
+
+use libusb1_sys::{libusb_get_version, libusb_version};
+
+/// A structure that describes the version of the underlying `libusb` library.
+pub struct LibraryVersion {
+    inner: &'static libusb_version,
+}
+
+impl LibraryVersion {
+    /// Library major version.
+    pub fn major(&self) -> u16 {
+        self.inner.major
+    }
+
+    /// Library minor version.
+    pub fn minor(&self) -> u16 {
+        self.inner.minor
+    }
+
+    /// Library micro version.
+    pub fn micro(&self) -> u16 {
+        self.inner.micro
+    }
+
+    /// Library nano version.
+    pub fn nano(&self) -> u16 {
+        self.inner.nano
+    }
+
+    /// Library release candidate suffix string, e.g., `"-rc4"`.
+    pub fn rc(&self) -> Option<&'static str> {
+        let cstr = unsafe { CStr::from_ptr(self.inner.rc) };
+
+        match str::from_utf8(cstr.to_bytes()) {
+            Ok(s) => {
+                if s.is_empty() {
+                    None
+                } else {
+                    Some(s)
+                }
+            }
+            Err(_) => None,
+        }
+    }
+}
+
+impl fmt::Debug for LibraryVersion {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        let mut debug = fmt.debug_struct("LibraryVersion");
+
+        debug.field("major", &self.major());
+        debug.field("minor", &self.minor());
+        debug.field("micro", &self.micro());
+        debug.field("nano", &self.nano());
+        debug.field("rc", &self.rc());
+
+        debug.finish()
+    }
+}
+
+/// Returns a structure with the version of the running libusb library.
+pub fn version() -> LibraryVersion {
+    let version: &'static libusb_version = unsafe { &*libusb_get_version() };
+
+    LibraryVersion { inner: version }
+}