Bug 1523417 - BITS client library for update downloading r=aklotz,emilio,froydnj
authorAdam Gashlin <agashlin@mozilla.com>
Thu, 21 Mar 2019 22:43:41 +0000
changeset 465558 ac38291d9c56a0aa6d9e3142261f90ac45e82224
parent 465557 d12b8cd1cee80c0c85d427bb2c507eaf240deabb
child 465559 0c95586ed7d524b6cace5478288bba1f5caca2f6
push id35744
push userapavel@mozilla.com
push dateFri, 22 Mar 2019 16:44:08 +0000
treeherdermozilla-central@e66a2b59914d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaklotz, emilio, froydnj
bugs1523417
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1523417 - BITS client library for update downloading r=aklotz,emilio,froydnj Differential Revision: https://phabricator.services.mozilla.com/D17989
Cargo.lock
third_party/rust/comedy/.cargo-checksum.json
third_party/rust/comedy/Cargo.toml
third_party/rust/comedy/LICENSE-APACHE
third_party/rust/comedy/LICENSE-MIT
third_party/rust/comedy/src/com.rs
third_party/rust/comedy/src/error.rs
third_party/rust/comedy/src/handle.rs
third_party/rust/comedy/src/lib.rs
third_party/rust/filetime_win/.cargo-checksum.json
third_party/rust/filetime_win/Cargo.toml
third_party/rust/filetime_win/LICENSE-APACHE
third_party/rust/filetime_win/LICENSE-MIT
third_party/rust/filetime_win/src/lib.rs
third_party/rust/guid_win/.cargo-checksum.json
third_party/rust/guid_win/Cargo.toml
third_party/rust/guid_win/LICENSE-APACHE
third_party/rust/guid_win/LICENSE-MIT
third_party/rust/guid_win/src/lib.rs
toolkit/components/bitsdownload/Cargo.toml
toolkit/components/bitsdownload/bits_client/.gitignore
toolkit/components/bitsdownload/bits_client/Cargo.toml
toolkit/components/bitsdownload/bits_client/README.md
toolkit/components/bitsdownload/bits_client/bits/Cargo.toml
toolkit/components/bitsdownload/bits_client/bits/src/callback.rs
toolkit/components/bitsdownload/bits_client/bits/src/lib.rs
toolkit/components/bitsdownload/bits_client/bits/src/status.rs
toolkit/components/bitsdownload/bits_client/bits/src/wide.rs
toolkit/components/bitsdownload/bits_client/examples/test_client.rs
toolkit/components/bitsdownload/bits_client/src/bits_protocol.rs
toolkit/components/bitsdownload/bits_client/src/in_process/mod.rs
toolkit/components/bitsdownload/bits_client/src/in_process/tests.rs
toolkit/components/bitsdownload/bits_client/src/lib.rs
toolkit/components/bitsdownload/src/lib.rs
toolkit/library/gtest/rust/Cargo.toml
toolkit/library/rust/Cargo.toml
toolkit/library/rust/gkrust-features.mozbuild
toolkit/library/rust/moz.build
toolkit/library/rust/shared/Cargo.toml
toolkit/library/rust/shared/lib.rs
toolkit/modules/AppConstants.jsm
toolkit/moz.configure
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -273,16 +273,50 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "bitreader"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
+name = "bits"
+version = "0.1.0"
+dependencies = [
+ "comedy 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "filetime_win 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "guid_win 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.80 (git+https://github.com/servo/serde?branch=deserialize_from_enums9)",
+ "winapi 0.3.6 (git+https://github.com/froydnj/winapi-rs?branch=aarch64)",
+]
+
+[[package]]
+name = "bits_client"
+version = "0.1.0"
+dependencies = [
+ "bits 0.1.0",
+ "comedy 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "guid_win 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "bitsdownload"
+version = "0.1.0"
+dependencies = [
+ "bits_client 0.1.0",
+]
+
+[[package]]
 name = "blake2-rfc"
 version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "arrayvec 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -442,16 +476,26 @@ dependencies = [
 name = "cmake"
 version = "0.1.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "cc 1.0.23 (git+https://github.com/glandium/cc-rs?branch=1.0.23-clang-cl-aarch64)",
 ]
 
 [[package]]
+name = "comedy"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.6 (git+https://github.com/froydnj/winapi-rs?branch=aarch64)",
+]
+
+[[package]]
 name = "constant_time_eq"
 version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "cookie"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -970,16 +1014,25 @@ source = "registry+https://github.com/ru
 name = "fallible"
 version = "0.0.1"
 dependencies = [
  "hashglobe 0.1.0",
  "smallvec 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
+name = "filetime_win"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "comedy 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.6 (git+https://github.com/froydnj/winapi-rs?branch=aarch64)",
+]
+
+[[package]]
 name = "fixedbitset"
 version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "flate2"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1136,16 +1189,17 @@ dependencies = [
 
 [[package]]
 name = "gkrust-shared"
 version = "0.1.0"
 dependencies = [
  "arrayvec 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "audioipc-client 0.4.0",
  "audioipc-server 0.2.3",
+ "bitsdownload 0.1.0",
  "cert_storage 0.0.1",
  "cose-c 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "cubeb-pulse 0.2.0",
  "cubeb-sys 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "encoding_c 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "encoding_glue 0.1.0",
  "env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "geckoservo 0.0.1",
@@ -1204,16 +1258,25 @@ version = "0.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "plain 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "scroll 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
+name = "guid_win"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "comedy 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.6 (git+https://github.com/froydnj/winapi-rs?branch=aarch64)",
+]
+
+[[package]]
 name = "h2"
 version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytes 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "futures 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3379,16 +3442,17 @@ dependencies = [
 "checksum cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "926013f2860c46252efceabb19f4a6b308197505082c609025aa6706c011d427"
 "checksum cc 1.0.23 (git+https://github.com/glandium/cc-rs?branch=1.0.23-clang-cl-aarch64)" = "<none>"
 "checksum cexpr 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8fc0086be9ca82f7fc89fc873435531cb898b86e850005850de1f820e2db6e9b"
 "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
 "checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878"
 "checksum clang-sys 0.26.1 (registry+https://github.com/rust-lang/crates.io-index)" = "481e42017c1416b1c0856ece45658ecbb7c93d8a93455f7e5fa77f3b35455557"
 "checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
 "checksum cmake 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "56d741ea7a69e577f6d06b36b7dff4738f680593dc27a701ffa8506b73ce28bb"
+"checksum comedy 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d4f03fbb05a4df3523a44cda10340e6ae6bea03ee9d01240a1a2c1ef6c73e95"
 "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e"
 "checksum cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1465f8134efa296b4c19db34d909637cb2bf0f7aaf21299e23e18fa29ac557cf"
 "checksum core-foundation 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4e2640d6d0bf22e82bed1b73c6aef8d5dd31e5abe6666c57e6d45e2649f4f887"
 "checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
 "checksum core-graphics 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "62ceafe1622ffc9a332199096841d0ff9912ec8cf8f9cde01e254a7d5217cd10"
 "checksum core-text 13.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f3f46450d6f2397261af420b4ccce23807add2e45fa206410a03d66fb7f050ae"
 "checksum cose 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "72fa26cb151d3ae4b70f63d67d0fed57ce04220feafafbae7f503bef7aae590d"
 "checksum cose-c 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "49726015ab0ca765144fcca61e4a7a543a16b795a777fa53f554da2fffff9a94"
@@ -3433,16 +3497,17 @@ dependencies = [
 "checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
 "checksum env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0561146661ae44c579e993456bc76d11ce1e0c7d745e57b2fa7146b6e49fa2ad"
 "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
 "checksum euclid 0.19.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d1a7698bdda3d7444a79d33bdc96e8b518d44ea3ff101d8492a6ca1207b886ea"
 "checksum euclid_macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdcb84c18ea5037a1c5a23039b4ff29403abce2e0d6b1daa11cf0bde2b30be15"
 "checksum failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6dd377bcc1b1b7ce911967e3ec24fa19c3224394ec05b54aa7b083d498341ac7"
 "checksum failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "64c2d913fe8ed3b6c6518eedf4538255b989945c14c2a7d5cbff62a5e2120596"
 "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+"checksum filetime_win 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8c37abd4a58e0cb794bcae4a7dc4f02fff376949d8d1066d4c729e97bfb38ec"
 "checksum fixedbitset 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "85cb8fec437468d86dc7c83ca7cfc933341d561873275f22dd5eedefa63a6478"
 "checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909"
 "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
 "checksum foreign-types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ebc04f19019fff1f2d627b5581574ead502f80c48c88900575a46e0840fe5d0"
 "checksum freetype 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b659e75b7a7338fe75afd7f909fc2b71937845cffb6ebe54ba2e50f13d8e903d"
 "checksum fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
 "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
@@ -3451,16 +3516,17 @@ dependencies = [
 "checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
 "checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb"
 "checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592"
 "checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d"
 "checksum gl_generator 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a0ffaf173cf76c73a73e080366bf556b4776ece104b06961766ff11449f38604"
 "checksum gleam 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)" = "17bca843dd3cf25db1bf415d55de9c0f0ae09dd7fa952ec3cef9930f90de1339"
 "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
 "checksum goblin 0.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "5911d7df7b8f65ab676c5327b50acea29d3c6a1a4ad05e444cf5dce321b26db2"
+"checksum guid_win 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "87261686cc5e35b6584f4c2a430c2b153d8a92ab1ef820c16be34c1df8f5f58b"
 "checksum h2 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "a27e7ed946e8335bdf9a191bc1b9b14a03ba822d013d2f58437f4fabcbd7fc2c"
 "checksum http 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "dca621d0fa606a5ff2850b6e337b57ad6137ee4d67e940449643ff45af6874c6"
 "checksum httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "af2f2dd97457e8fb1ae7c5a420db346af389926e36f43768b96f101546b04a07"
 "checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e"
 "checksum hyper 0.12.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c087746de95e20e4dabe86606c3a019964a8fde2d5f386152939063c116c5971"
 "checksum ident_case 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c9826188e666f2ed92071d2dadef6edc430b11b158b5b2b3f4babbcc891eaaa"
 "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
 "checksum indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08173ba1e906efb6538785a8844dd496f5d34f0a2d88038e95195172fc667220"
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/.cargo-checksum.json
@@ -0,0 +1,1 @@
+{"files":{"Cargo.toml":"a25c39d7d86369f1dfa790b47ce678385112229942a489b761287f35565a76e1","LICENSE-APACHE":"b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1","LICENSE-MIT":"9eeff459e61e81ab28896aa2df8545fe15dd6bfe0dc87c3384fdbc8b1b146661","src/com.rs":"5407ae11d8f4e37e2dee74c1f92ecc63e27c98c0bce9822cd4216450ea050c2d","src/error.rs":"7dcf0b47b5ee4fcdf79444d8bba3f736ad83cc7c5ce77da446e23f81cc5f10ed","src/handle.rs":"190a0e8fc8f00f4d0e21c0d2098773ff5384284d7967f92bae7226de6155c6cb","src/lib.rs":"ab892c28ba11400125626bf85487a2033b4784fa29877d2a5e8e0868edf19df8"},"package":"6d4f03fbb05a4df3523a44cda10340e6ae6bea03ee9d01240a1a2c1ef6c73e95"}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/Cargo.toml
@@ -0,0 +1,37 @@
+# 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 believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+name = "comedy"
+version = "0.1.0"
+authors = ["Adam Gashlin <agashlin@mozilla.com>"]
+description = "Windows error handling, COM, and handles"
+keywords = ["windows", "com", "win32"]
+categories = ["api-bindings", "os::windows-apis"]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/agashlin/comedy-rs"
+[package.metadata.docs.rs]
+default-target = "x86_64-pc-windows-msvc"
+[dependencies.failure]
+version = "0.1.3"
+features = ["derive"]
+default_features = false
+
+[dependencies.failure_derive]
+version = "0.1.3"
+
+[dependencies.winapi]
+version = "0.3.6"
+features = ["basetsd", "combaseapi", "errhandlingapi", "handleapi", "impl-default", "minwindef", "objbase", "unknwnbase", "winbase", "winerror", "wtypes", "wtypesbase"]
+[dev-dependencies.winapi]
+version = "0.3.6"
+features = ["bits", "fileapi", "guiddef"]
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright (c) 2019 The winapi-rs and comedy Developers
+
+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.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/src/com.rs
@@ -0,0 +1,478 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Utilities and wrappers for Microsoft COM interfaces in Windows.
+//!
+//! This works with the `Class` and `Interface` traits from the `winapi` crate.
+
+use std::marker::PhantomData;
+use std::mem;
+use std::ops::Deref;
+use std::ptr::{self, null_mut, NonNull};
+use std::rc::Rc;
+use std::slice;
+
+use winapi::shared::minwindef::LPVOID;
+use winapi::shared::{
+    winerror::HRESULT,
+    wtypesbase::{CLSCTX, CLSCTX_INPROC_SERVER, CLSCTX_LOCAL_SERVER},
+};
+use winapi::um::{
+    combaseapi::{CoCreateInstance, CoInitializeEx, CoTaskMemFree, CoUninitialize},
+    objbase::{COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED},
+    unknwnbase::IUnknown,
+};
+use winapi::{Class, Interface};
+
+use check_succeeded;
+use error::{succeeded_or_err, HResult, ResultExt};
+
+/// Wrap COM interfaces sanely
+///
+/// Originally from [wio-rs](https://github.com/retep998/wio-rs) `ComPtr`
+#[repr(transparent)]
+pub struct ComRef<T>(NonNull<T>)
+where
+    T: Interface;
+impl<T> ComRef<T>
+where
+    T: Interface,
+{
+    /// Creates a `ComRef` to wrap a raw pointer.
+    /// It takes ownership over the pointer which means it does __not__ call `AddRef`.
+    /// `T` __must__ be a COM interface that inherits from `IUnknown`.
+    pub unsafe fn from_raw(ptr: NonNull<T>) -> ComRef<T> {
+        ComRef(ptr)
+    }
+
+    /// Casts up the inheritance chain
+    pub fn up<U>(self) -> ComRef<U>
+    where
+        T: Deref<Target = U>,
+        U: Interface,
+    {
+        unsafe { ComRef::from_raw(NonNull::new(self.into_raw().as_ptr() as *mut U).unwrap()) }
+    }
+
+    /// Extracts the raw pointer.
+    /// You are now responsible for releasing it yourself.
+    pub fn into_raw(self) -> NonNull<T> {
+        let p = self.0;
+        mem::forget(self);
+        p
+    }
+
+    fn as_unknown(&self) -> &IUnknown {
+        unsafe { &*(self.as_raw_ptr() as *mut IUnknown) }
+    }
+
+    /// Get another interface via `QueryInterface`.
+    ///
+    /// If the call to `QueryInterface` fails or the resulting interface is null, return an error.
+    pub fn cast<U>(&self) -> Result<ComRef<U>, HResult>
+    where
+        U: Interface,
+    {
+        let mut obj = null_mut();
+        let hr =
+            succeeded_or_err(unsafe { self.as_unknown().QueryInterface(&U::uuidof(), &mut obj) })?;
+        NonNull::new(obj as *mut U)
+            .map(|obj| unsafe { ComRef::from_raw(obj) })
+            .ok_or_else(|| HResult::new(hr).function("IUnknown::QueryInterface"))
+    }
+
+    /// Obtains the raw pointer without transferring ownership.
+    ///
+    /// Do __not__ release this pointer because it is still owned by the `ComRef`.
+    ///
+    /// Do __not__ use this pointer beyond the lifetime of the `ComRef`.
+    pub fn as_raw(&self) -> NonNull<T> {
+        self.0
+    }
+
+    /// Obtains the raw pointer without transferring ownership.
+    ///
+    /// Do __not__ release this pointer because it is still owned by the `ComRef`.
+    ///
+    /// Do __not__ use this pointer beyond the lifetime of the `ComRef`.
+    pub fn as_raw_ptr(&self) -> *mut T {
+        self.0.as_ptr()
+    }
+}
+impl<T> Deref for ComRef<T>
+where
+    T: Interface,
+{
+    type Target = T;
+    fn deref(&self) -> &T {
+        unsafe { &*self.as_raw_ptr() }
+    }
+}
+impl<T> Clone for ComRef<T>
+where
+    T: Interface,
+{
+    fn clone(&self) -> Self {
+        unsafe {
+            self.as_unknown().AddRef();
+            ComRef::from_raw(self.as_raw())
+        }
+    }
+}
+impl<T> Drop for ComRef<T>
+where
+    T: Interface,
+{
+    fn drop(&mut self) {
+        unsafe {
+            self.as_unknown().Release();
+        }
+    }
+}
+
+/// A scope for automatic COM initialization and deinitialization.
+///
+/// Functions that need COM initialized can take a `&ComApartmentScope` argument and be sure that
+/// it is so. It's recommended to use a thread local for this through the
+/// [`INIT_MTA`](constant.INIT_MTA.html) or [`INIT_STA`](constant.INIT_STA.html) statics.
+#[derive(Debug, Default)]
+pub struct ComApartmentScope {
+    /// PhantomData used in lieu of unstable impl !Send + !Sync.
+    /// It must be dropped on the same thread it was created on so it can't be Send,
+    /// and references are meant to indicate that COM has been inited on the current thread so it
+    /// can't be Sync.
+    _do_not_send: PhantomData<Rc<()>>,
+}
+
+impl ComApartmentScope {
+    /// This thread should be the sole occupant of a single thread apartment
+    pub fn init_sta() -> Result<Self, HResult> {
+        unsafe { check_succeeded!(CoInitializeEx(ptr::null_mut(), COINIT_APARTMENTTHREADED)) }?;
+
+        Ok(Default::default())
+    }
+
+    /// This thread should join the process's multithreaded apartment
+    pub fn init_mta() -> Result<Self, HResult> {
+        unsafe { check_succeeded!(CoInitializeEx(ptr::null_mut(), COINIT_MULTITHREADED)) }?;
+
+        Ok(Default::default())
+    }
+}
+
+impl Drop for ComApartmentScope {
+    fn drop(&mut self) {
+        unsafe {
+            CoUninitialize();
+        }
+    }
+}
+
+thread_local! {
+    // TODO these examples should probably be in convenience functions.
+    /// A single thread apartment scope for the duration of the current thread.
+    ///
+    /// # Example
+    /// ```
+    /// use comedy::com::{ComApartmentScope, INIT_STA};
+    ///
+    /// fn do_com_stuff(_com: &ComApartmentScope) {
+    /// }
+    ///
+    /// INIT_STA.with(|com| {
+    ///     let com = match com {
+    ///         Err(e) => return Err(e.clone()),
+    ///         Ok(ref com) => com,
+    ///     };
+    ///     do_com_stuff(com);
+    ///     Ok(())
+    /// }).unwrap()
+    /// ```
+    pub static INIT_STA: Result<ComApartmentScope, HResult> = ComApartmentScope::init_sta();
+
+    /// A multithreaded apartment scope for the duration of the current thread.
+    ///
+    /// # Example
+    /// ```
+    /// use comedy::com::{ComApartmentScope, INIT_MTA};
+    ///
+    /// fn do_com_stuff(_com: &ComApartmentScope) {
+    /// }
+    ///
+    /// INIT_MTA.with(|com| {
+    ///     let com = match com {
+    ///         Err(e) => return Err(e.clone()),
+    ///         Ok(ref com) => com,
+    ///     };
+    ///     do_com_stuff(com);
+    ///     Ok(())
+    /// }).unwrap()
+    /// ```
+    pub static INIT_MTA: Result<ComApartmentScope, HResult> = ComApartmentScope::init_mta();
+}
+
+/// Create an instance of a COM class.
+///
+/// This is mostly just a call to `CoCreateInstance` with some error handling.
+/// The CLSID of the class and the IID of the interface come from the winapi `RIDL` macro, which
+/// defines `Class` and `Interface` implementations.
+///
+/// [`create_instance_local_server`](fn.create_instance_local_server.html) and
+/// [`create_instance_inproc_server`](fn.create_instance_inproc_server.html) are convenience
+/// functions for typical contexts.
+pub fn create_instance<C, I>(ctx: CLSCTX) -> Result<ComRef<I>, HResult>
+where
+    C: Class,
+    I: Interface,
+{
+    get(|interface| unsafe {
+        CoCreateInstance(
+            &C::uuidof(),
+            ptr::null_mut(), // pUnkOuter
+            ctx,
+            &I::uuidof(),
+            interface as *mut *mut _,
+        )
+    })
+    .function("CoCreateInstance")
+}
+
+/// Create an instance of a COM class in the current process (`CLSCTX_LOCAL_SERVER`).
+pub fn create_instance_local_server<C, I>() -> Result<ComRef<I>, HResult>
+where
+    C: Class,
+    I: Interface,
+{
+    create_instance::<C, I>(CLSCTX_LOCAL_SERVER)
+}
+
+/// Create an instance of a COM class in a separate process space on the same machine
+/// (`CLSCTX_INPROC_SERVER`).
+pub fn create_instance_inproc_server<C, I>() -> Result<ComRef<I>, HResult>
+where
+    C: Class,
+    I: Interface,
+{
+    create_instance::<C, I>(CLSCTX_INPROC_SERVER)
+}
+
+/// Call a COM method, returning a `Result`.
+///
+/// An error is returned if the call fails. The error is augmented with the name of the interface
+/// and method, and the file name and line number of the macro usage.
+///
+/// `QueryInterface` is not used, the receiving interface must already be the given type.
+///
+/// # Example
+///
+/// ```no_run
+/// # extern crate winapi;
+/// #
+/// # use winapi::um::bits::IBackgroundCopyJob;
+/// #
+/// # use comedy::com::ComRef;
+/// # use comedy::{com_call, HResult};
+/// #
+/// fn cancel_job(job: &ComRef<IBackgroundCopyJob>) -> Result<(), HResult> {
+///     unsafe {
+///         com_call!(job, IBackgroundCopyJob::Cancel())?;
+///     }
+///     Ok(())
+/// }
+/// ```
+#[macro_export]
+macro_rules! com_call {
+    ($obj:expr, $interface:ident :: $method:ident ( $($arg:expr),* )) => {{
+        use $crate::error::ResultExt;
+        $crate::error::succeeded_or_err({
+            let obj: &$interface = &*$obj;
+            obj.$method($($arg),*)
+        }).function(concat!(stringify!($interface), "::", stringify!($method)))
+          .file_line(file!(), line!())
+    }};
+
+    // support for trailing comma in method argument list
+    ($obj:expr, $interface:ident :: $method:ident ( $($arg:expr),+ , )) => {{
+        $crate::com_call!($obj, $interface::$method($($arg),+))
+    }};
+}
+
+/// Get an interface pointer that is returned through an output parameter.
+///
+/// If the call returns a failure `HRESULT`, return an error.
+///
+///
+/// # Null and Enumerators
+/// If the method succeeds but the resulting interface pointer is null, this will return an
+/// `HResult` error with the successful return code. In particular this can happen with
+/// enumeration interfaces, which return `S_FALSE` when they write less than the requested number
+/// of results.
+pub fn get<I, F>(getter: F) -> Result<ComRef<I>, HResult>
+where
+    I: Interface,
+    F: FnOnce(*mut *mut I) -> HRESULT,
+{
+    let mut interface: *mut I = ptr::null_mut();
+
+    let hr = succeeded_or_err(getter(&mut interface as *mut *mut I))?;
+
+    NonNull::new(interface)
+        .map(|interface| unsafe { ComRef::from_raw(interface) })
+        .ok_or_else(|| HResult::new(hr))
+}
+
+/// Call a COM method, create a [`ComRef`](com/struct.ComRef.html) from an output parameter.
+///
+/// An error is returned if the call fails. The error is augmented with the name of the interface
+/// and method, and the file name and line number of the macro usage.
+///
+/// # Null and Enumerators
+/// If the method succeeds but the resulting interface pointer is null, this will return an
+/// `HResult` error with the successful return code. In particular this can happen with
+/// enumeration interfaces, which return `S_FALSE` when they write less than the requested number
+/// of results.
+///
+/// # Example
+///
+/// ```no_run
+/// # extern crate winapi;
+/// # use winapi::shared::guiddef::GUID;
+/// # use winapi::um::bits::{IBackgroundCopyManager, IBackgroundCopyJob};
+/// # use comedy::com::ComRef;
+/// # use comedy::{com_call_getter, HResult};
+///
+/// fn create_job(bcm: &ComRef<IBackgroundCopyManager>, id: &GUID)
+///     -> Result<ComRef<IBackgroundCopyJob>, HResult>
+/// {
+///     unsafe {
+///         com_call_getter!(
+///             |job| bcm,
+///             IBackgroundCopyManager::GetJob(id, job)
+///         )
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! com_call_getter {
+    (| $outparam:ident | $obj:expr, $interface:ident :: $method:ident ( $($arg:expr),* )) => {{
+        use $crate::error::ResultExt;
+        let obj: &$interface = &*$obj;
+        $crate::com::get(|$outparam| {
+            obj.$method($($arg),*)
+        }).function(concat!(stringify!($interface), "::", stringify!($method)))
+          .file_line(file!(), line!())
+    }};
+
+    // support for trailing comma in method argument list
+    (| $outparam:ident | $obj:expr, $interface:ident :: $method:ident ( $($arg:expr),+ , )) => {
+        $crate::com_call_getter!(|$outparam| $obj, $interface::$method($($arg),+))
+    };
+}
+
+/// Get a task memory pointer that is returned through an output parameter.
+///
+/// If the call returns a failure `HRESULT`, return an error.
+///
+/// # Null
+/// If the method succeeds but the resulting pointer is null, this will return an
+/// `Err(HResult)` with the successful return code.
+pub fn get_cotaskmem<F, T>(getter: F) -> Result<CoTaskMem<T>, HResult>
+where
+    F: FnOnce(*mut *mut T) -> HRESULT,
+{
+    let mut ptr = ptr::null_mut() as *mut T;
+
+    let hr = succeeded_or_err(getter(&mut ptr))?;
+
+    NonNull::new(ptr)
+        .map(|ptr| unsafe { CoTaskMem::new(ptr) })
+        .ok_or_else(|| HResult::new(hr))
+}
+
+/// Call a COM method, create a [`CoTaskMem`](handle/struct.CoTaskMem.html) from an output
+/// parameter.
+///
+/// An error is returned if the call fails or if the pointer is null. The error is augmented with
+/// the name of the interface and method, and the file name and line number of the macro usage.
+///
+/// # Null
+/// If the method succeeds but the resulting pointer is null, this will return an `HResult`
+/// error with the successful return code.
+///
+/// # Example
+///
+/// ```no_run
+/// # extern crate winapi;
+/// # use winapi::shared::winerror::HRESULT;
+/// # use winapi::um::bits::IBackgroundCopyManager;
+/// # use comedy::com::{ComRef, CoTaskMem};
+/// # use comedy::{com_call_taskmem_getter, HResult};
+/// #
+/// fn get_error_description(bcm: &ComRef<IBackgroundCopyManager>, lang_id: u32, hr: HRESULT)
+///     -> Result<CoTaskMem<u16>, HResult>
+/// {
+///     unsafe {
+///         com_call_taskmem_getter!(
+///             |desc| bcm,
+///             IBackgroundCopyManager::GetErrorDescription(hr, lang_id, desc)
+///         )
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! com_call_taskmem_getter {
+    (| $outparam:ident | $obj:expr, $interface:ident :: $method:ident ( $($arg:expr),* )) => {{
+        use $crate::error::ResultExt;
+        $crate::com::get_cotaskmem(|$outparam| {
+            $obj.$method($($arg),*)
+        }).function(concat!(stringify!($interface), "::", stringify!($method)))
+          .file_line(file!(), line!())
+    }};
+
+    // support for trailing comma in method argument list
+    (| $outparam:ident | $obj:expr, $interface:ident :: $method:ident ( $($arg:expr),+ , )) => {
+        $crate::com_call_taskmem_getter!(|$outparam| $obj, $interface::$method($($arg),+))
+    };
+}
+
+/// A Windows COM task memory pointer that will be automatically freed.
+// I have only found this useful with strings, so that is the only access provided here by
+// `as_slice_until_null()`. `Deref<Target=T>` would not be generally useful as task memory
+// is usually encountered as variable length structures.
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct CoTaskMem<T>(NonNull<T>);
+
+impl<T> CoTaskMem<T> {
+    /// Take ownership of COM task memory, which will be freed with `CoTaskMemFree()` upon drop.
+    ///
+    /// # Safety
+    ///
+    /// `p` should be the only copy of the pointer.
+    pub unsafe fn new(p: NonNull<T>) -> CoTaskMem<T> {
+        CoTaskMem(p)
+    }
+}
+
+impl<T> Drop for CoTaskMem<T> {
+    fn drop(&mut self) {
+        unsafe {
+            CoTaskMemFree(self.0.as_ptr() as LPVOID);
+        }
+    }
+}
+
+impl CoTaskMem<u16> {
+    /// Interpret as a null-terminated `u16` array, return as a slice without terminator.
+    pub unsafe fn as_slice_until_null(&self) -> &[u16] {
+        for i in 0.. {
+            if *self.0.as_ptr().offset(i) == 0 {
+                return slice::from_raw_parts(self.0.as_ptr(), i as usize);
+            }
+        }
+        unreachable!()
+    }
+}
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/src/error.rs
@@ -0,0 +1,376 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Wrap several flavors of Windows error into a `Result`.
+
+use std::fmt;
+
+use failure::Fail;
+
+use winapi::shared::minwindef::DWORD;
+use winapi::shared::winerror::{
+    ERROR_SUCCESS, FACILITY_WIN32, HRESULT, HRESULT_FROM_WIN32, SUCCEEDED, S_OK,
+};
+use winapi::um::errhandlingapi::GetLastError;
+
+/// An error code, optionally with information about the failing call.
+#[derive(Clone, Debug, Eq, Fail, PartialEq)]
+pub struct ErrorAndSource<T: ErrorCode> {
+    code: T,
+    function: Option<&'static str>,
+    file_line: Option<FileLine>,
+}
+
+/// A wrapper for an error code.
+pub trait ErrorCode:
+    Copy + fmt::Debug + Eq + PartialEq + fmt::Display + Send + Sync + 'static
+{
+    type InnerT: Copy + Eq + PartialEq;
+
+    fn get(&self) -> Self::InnerT;
+}
+
+impl<T> ErrorAndSource<T>
+where
+    T: ErrorCode,
+{
+    /// Get the underlying error code.
+    pub fn code(&self) -> T::InnerT {
+        self.code.get()
+    }
+
+    /// Add the name of the failing function to the error.
+    pub fn function(self, function: &'static str) -> Self {
+        Self {
+            function: Some(function),
+            ..self
+        }
+    }
+
+    /// Get the name of the failing function, if known.
+    pub fn get_function(&self) -> Option<&'static str> {
+        self.function
+    }
+
+    /// Add the source file name and line number of the call to the error.
+    pub fn file_line(self, file: &'static str, line: u32) -> Self {
+        Self {
+            file_line: Some(FileLine(file, line)),
+            ..self
+        }
+    }
+
+    /// Get the source file name and line number of the failing call.
+    pub fn get_file_line(&self) -> &Option<FileLine> {
+        &self.file_line
+    }
+}
+
+impl<T> fmt::Display for ErrorAndSource<T>
+where
+    T: ErrorCode,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        if let Some(function) = self.function {
+            if let Some(ref file_line) = self.file_line {
+                write!(f, "{} ", file_line)?;
+            }
+
+            write!(f, "{} ", function)?;
+
+            write!(f, "error: ")?;
+        }
+
+        write!(f, "{}", self.code)?;
+
+        Ok(())
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct FileLine(pub &'static str, pub u32);
+
+impl fmt::Display for FileLine {
+    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        write!(f, "{}:{}", self.0, self.1)
+    }
+}
+
+/// A [Win32 error code](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d),
+/// usually from `GetLastError()`.
+///
+/// Includes optional function name, source file name, and line number. See
+/// [`ErrorAndSource`](struct.ErrorAndSource.html) for additional methods.
+pub type Win32Error = ErrorAndSource<Win32ErrorInner>;
+
+impl Win32Error {
+    /// Create from an error code.
+    pub fn new(code: DWORD) -> Self {
+        Win32Error {
+            code: Win32ErrorInner(code),
+            function: None,
+            file_line: None,
+        }
+    }
+
+    /// Create from `GetLastError()`
+    pub fn get_last_error() -> Self {
+        Win32Error::new(unsafe { GetLastError() })
+    }
+}
+
+#[doc(hidden)]
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct Win32ErrorInner(DWORD);
+
+impl ErrorCode for Win32ErrorInner {
+    type InnerT = DWORD;
+
+    fn get(&self) -> DWORD {
+        self.0
+    }
+}
+
+impl fmt::Display for Win32ErrorInner {
+    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        write!(f, "{:#010x}", self.0)
+    }
+}
+
+/// An [HRESULT error code](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a).
+/// These usually come from COM APIs.
+///
+/// Includes optional function name, source file name, and line number. See
+/// [`ErrorAndSource`](struct.ErrorAndSource.html) for additional methods.
+pub type HResult = ErrorAndSource<HResultInner>;
+
+impl HResult {
+    /// Create from an `HRESULT`.
+    pub fn new(hr: HRESULT) -> Self {
+        HResult {
+            code: HResultInner(hr),
+            function: None,
+            file_line: None,
+        }
+    }
+
+    /// Get the result code portion of the `HRESULT`
+    pub fn extract_code(&self) -> HRESULT {
+        // from winerror.h HRESULT_CODE macro
+        self.code.0 & 0xFFFF
+    }
+
+    /// Get the facility portion of the `HRESULT`
+    pub fn extract_facility(&self) -> HRESULT {
+        // from winerror.h HRESULT_FACILITY macro
+        (self.code.0 >> 16) & 0x1fff
+    }
+
+    /// If the `HResult` corresponds to a Win32 error, convert.
+    ///
+    /// Returns the original `HResult` as an error on failure.
+    pub fn try_into_win32_err(self) -> Result<Win32Error, Self> {
+        let code = if self.code() == S_OK {
+            // Special case, facility is not set.
+            ERROR_SUCCESS
+        } else if self.extract_facility() == FACILITY_WIN32 {
+            self.extract_code() as DWORD
+        } else {
+            return Err(self);
+        };
+
+        Ok(Win32Error {
+            code: Win32ErrorInner(code),
+            function: self.function,
+            file_line: self.file_line,
+        })
+    }
+}
+
+#[doc(hidden)]
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct HResultInner(HRESULT);
+
+impl ErrorCode for HResultInner {
+    type InnerT = HRESULT;
+
+    fn get(&self) -> HRESULT {
+        self.0
+    }
+}
+
+impl fmt::Display for HResultInner {
+    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        write!(f, "HRESULT {:#010x}", self.0)
+    }
+}
+
+/// Extra functions to work with a `Result<T, ErrorAndSource>`.
+pub trait ResultExt<T, E> {
+    type Code;
+
+    /// Add the name of the failing function to the error.
+    fn function(self, function: &'static str) -> Self;
+
+    /// Add the source file name and line number of the call to the error.
+    fn file_line(self, file: &'static str, line: u32) -> Self;
+
+    /// Replace `Err(code)` with `Ok(replacement)`.
+    fn allow_err(self, code: Self::Code, replacement: T) -> Self;
+
+    /// Replace `Err(code)` with `Ok(replacement())`.
+    fn allow_err_with<F>(self, code: Self::Code, replacement: F) -> Self
+    where
+        F: FnOnce() -> T;
+}
+
+impl<T, EC> ResultExt<T, ErrorAndSource<EC>> for Result<T, ErrorAndSource<EC>>
+where
+    EC: ErrorCode,
+{
+    type Code = EC::InnerT;
+
+    fn function(self, function: &'static str) -> Self {
+        self.map_err(|e| e.function(function))
+    }
+
+    fn file_line(self, file: &'static str, line: u32) -> Self {
+        self.map_err(|e| e.file_line(file, line))
+    }
+
+    fn allow_err(self, code: Self::Code, replacement: T) -> Self {
+        self.or_else(|e| {
+            if e.code() == code {
+                Ok(replacement)
+            } else {
+                Err(e)
+            }
+        })
+    }
+
+    fn allow_err_with<F>(self, code: Self::Code, replacement: F) -> Self
+    where
+        F: FnOnce() -> T,
+    {
+        self.or_else(|e| {
+            if e.code() == code {
+                Ok(replacement())
+            } else {
+                Err(e)
+            }
+        })
+    }
+}
+
+impl From<Win32Error> for HResult {
+    fn from(win32_error: Win32Error) -> Self {
+        HResult {
+            code: HResultInner(HRESULT_FROM_WIN32(win32_error.code())),
+            function: win32_error.function,
+            file_line: win32_error.file_line,
+        }
+    }
+}
+
+/// Convert an `HRESULT` into a `Result`.
+pub fn succeeded_or_err(hr: HRESULT) -> Result<HRESULT, HResult> {
+    if !SUCCEEDED(hr) {
+        Err(HResult::new(hr))
+    } else {
+        Ok(hr)
+    }
+}
+
+/// Call a function that returns an `HRESULT`, convert to a `Result`.
+///
+/// The error will be augmented with the name of the function and the file and line number of
+/// the macro usage.
+///
+/// # Example
+/// ```no_run
+/// # extern crate winapi;
+/// # use std::ptr;
+/// # use winapi::um::combaseapi::CoUninitialize;
+/// # use winapi::um::objbase::CoInitialize;
+/// # use comedy::{check_succeeded, HResult};
+/// #
+/// fn coinit() -> Result<(), HResult> {
+///     unsafe {
+///         check_succeeded!(CoInitialize(ptr::null_mut()))?;
+///
+///         CoUninitialize();
+///     }
+///     Ok(())
+/// }
+/// ```
+#[macro_export]
+macro_rules! check_succeeded {
+    ($f:ident ( $($arg:expr),* )) => {
+        {
+            use $crate::error::ResultExt;
+            $crate::error::succeeded_or_err($f($($arg),*))
+                .function(stringify!($f))
+                .file_line(file!(), line!())
+        }
+    };
+
+    // support for trailing comma in argument list
+    ($f:ident ( $($arg:expr),+ , )) => {
+        $crate::check_succeeded!($f($($arg),+))
+    };
+}
+
+/// Convert an integer return value into a `Result`, using `GetLastError()` if zero.
+pub fn true_or_last_err<T>(rv: T) -> Result<T, Win32Error>
+where
+    T: Eq,
+    T: From<bool>,
+{
+    if rv == T::from(false) {
+        Err(Win32Error::get_last_error())
+    } else {
+        Ok(rv)
+    }
+}
+
+/// Call a function that returns a integer, convert to a `Result`, using `GetLastError()` if zero.
+///
+/// The error will be augmented with the name of the function and the file and line number of
+/// the macro usage.
+///
+/// # Example
+/// ```no_run
+/// # extern crate winapi;
+/// # use winapi::shared::minwindef::BOOL;
+/// # use winapi::um::fileapi::FlushFileBuffers;
+/// # use winapi::um::winnt::HANDLE;
+/// # use comedy::{check_true, Win32Error};
+/// #
+/// fn flush(file: HANDLE) -> Result<(), Win32Error> {
+///     unsafe {
+///         check_true!(FlushFileBuffers(file))?;
+///     }
+///     Ok(())
+/// }
+/// ```
+#[macro_export]
+macro_rules! check_true {
+    ($f:ident ( $($arg:expr),* )) => {
+        {
+            use $crate::error::ResultExt;
+            $crate::error::true_or_last_err($f($($arg),*))
+                .function(stringify!($f))
+                .file_line(file!(), line!())
+        }
+    };
+
+    // support for trailing comma in argument list
+    ($f:ident ( $($arg:expr),+ , )) => {
+        $crate::check_true!($f($($arg),+))
+    };
+}
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/src/handle.rs
@@ -0,0 +1,91 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Wrapping and automatically closing handles.
+
+use winapi::shared::minwindef::DWORD;
+use winapi::shared::ntdef::NULL;
+use winapi::um::errhandlingapi::GetLastError;
+use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
+use winapi::um::winnt::HANDLE;
+
+/// Check and automatically close a Windows `HANDLE`.
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct Handle(HANDLE);
+
+impl Handle {
+    /// Take ownership of a `HANDLE`, which will be closed with `CloseHandle` upon drop.
+    /// Returns an error in case of `INVALID_HANDLE_VALUE` or `NULL`.
+    ///
+    /// # Safety
+    ///
+    /// `h` should be the only copy of the handle. `GetLastError()` is called to
+    /// return an error, so the last Windows API called on this thread should have been
+    /// what produced the invalid handle.
+    pub unsafe fn new(h: HANDLE) -> Result<Handle, DWORD> {
+        if h == NULL || h == INVALID_HANDLE_VALUE {
+            Err(GetLastError())
+        } else {
+            Ok(Handle(h))
+        }
+    }
+
+    /// Obtains the raw `HANDLE` without transferring ownership.
+    ///
+    /// Do __not__ close this handle because it is still owned by the `Handle`.
+    ///
+    /// Do __not__ use this handle beyond the lifetime of the `Handle`.
+    pub fn as_raw(&self) -> HANDLE {
+        self.0
+    }
+}
+
+impl Drop for Handle {
+    fn drop(&mut self) {
+        unsafe {
+            CloseHandle(self.0);
+        }
+    }
+}
+
+/// Call a function that returns a `HANDLE` (`NULL` or `INVALID_HANDLE_VALUE` on failure), wrap result.
+///
+/// The handle is wrapped in a [`Handle`](handle/struct.Handle.html) which will automatically call
+/// `CloseHandle()` on it. If the function fails, the error is retrieved via `GetLastError()` and
+/// augmented with the name of the function and the file and line number of the macro usage.
+///
+/// # Example
+///
+/// ```no_run
+/// # extern crate winapi;
+/// # use std::ptr;
+/// # use winapi::um::fileapi::FindFirstFileW;
+/// # use winapi::um::minwinbase::WIN32_FIND_DATAW;
+/// # use comedy::handle::Handle;
+/// # use comedy::{call_handle_getter, Win32Error};
+/// #
+/// unsafe fn find_first(name: &[u16], data: &mut WIN32_FIND_DATAW) -> Result<Handle, Win32Error> {
+///     call_handle_getter!(FindFirstFileW(name.as_ptr(), data))
+/// }
+/// ```
+#[macro_export]
+macro_rules! call_handle_getter {
+    ($f:ident ( $($arg:expr),* )) => {
+        {
+            use $crate::error::{ErrorCode, FileLine, ResultExt, Win32Error};
+            $crate::handle::Handle::new($f($($arg),*))
+                .map_err(Win32Error::new)
+                    .function(stringify!($f))
+                    .file_line(file!(), line!())
+        }
+    };
+
+    // support for trailing comma in argument list
+    ($f:ident ( $($arg:expr),+ , )) => {
+        $crate::call_valid_handle_getter!($f($($arg),*))
+    };
+}
new file mode 100644
--- /dev/null
+++ b/third_party/rust/comedy/src/lib.rs
@@ -0,0 +1,20 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+#![cfg(windows)]
+
+//! Windows error handling, COM, and handles
+//!
+//! See macros for examples.
+
+extern crate failure;
+extern crate failure_derive;
+extern crate winapi;
+
+pub mod com;
+pub mod error;
+pub mod handle;
+
+pub use error::{HResult, ResultExt, Win32Error};
new file mode 100644
--- /dev/null
+++ b/third_party/rust/filetime_win/.cargo-checksum.json
@@ -0,0 +1,1 @@
+{"files":{"Cargo.toml":"8bc102ef8b9c9d1e04cdf5fc3de07f015ec997666e2b78304ebb72e0904169e2","LICENSE-APACHE":"b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1","LICENSE-MIT":"3960e3d811fb354377e71b964acea3c00dafda2a5791fdd9f098b4db5f4022e4","src/lib.rs":"1e67ce8e1f6c4c755d8acd307bfa8af800f46817b466b28a5f982a2c32d21b9f"},"package":"b8c37abd4a58e0cb794bcae4a7dc4f02fff376949d8d1066d4c729e97bfb38ec"}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/third_party/rust/filetime_win/Cargo.toml
@@ -0,0 +1,40 @@
+# 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 believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+name = "filetime_win"
+version = "0.1.0"
+authors = ["Adam Gashlin <agashlin@mozilla.com>"]
+description = "Windows FILETIME and SYSTEMTIME string and binary serialization"
+keywords = ["windows", "com", "win32"]
+categories = ["api-bindings", "date-and-time", "os::windows-apis"]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/agashlin/filetime_win"
+[package.metadata.docs.rs]
+default-target = "x86_64-pc-windows-msvc"
+[dependencies.comedy]
+version = "0.1.0"
+
+[dependencies.serde]
+version = "1.0.80"
+optional = true
+
+[dependencies.serde_derive]
+version = "1.0.80"
+optional = true
+
+[dependencies.winapi]
+version = "0.3.6"
+features = ["minwinbase", "minwindef", "ntdef", "sysinfoapi", "timezoneapi"]
+
+[features]
+filetime_serde = ["serde", "serde_derive"]
new file mode 100644
--- /dev/null
+++ b/third_party/rust/filetime_win/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/filetime_win/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright (c) 2019 The filetime_win Developers
+
+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.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/filetime_win/src/lib.rs
@@ -0,0 +1,268 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Windows [`FILETIME`](https://docs.microsoft.com/en-us/windows/desktop/api/minwinbase/ns-minwinbase-filetime)
+//! and [`SYSTEMTIME`](https://docs.microsoft.com/en-us/windows/desktop/api/minwinbase/ns-minwinbase-systemtime)
+//! string and binary serialization
+//!
+//! A transparent wrapper is provided for each type, with
+//! `Display` for [`SystemTimeUTC`](struct.SystemTimeUTC.html) and
+//! `Ord` and `Eq` for [`FileTime`](struct.FileTime.html).
+//!
+//! # serde #
+//!
+//! Use the `filetime_serde` feature to derive `Serialize` and `Deserialize`, you can then
+//! derive them for structs containing `FILETIME` and `SYSTEMTIME` like so:
+//!
+//! ```
+//! # fn main() {}
+//! #
+//! # #[cfg(feature = "filetime_serde")]
+//! # extern crate serde_derive;
+//! # extern crate winapi;
+//! #
+//! # #[cfg(feature = "filetime_serde")]
+//! # mod test {
+//! use filetime_win::{FileTimeSerde, SystemTimeSerde};
+//! use serde_derive::{Deserialize, Serialize};
+//! use winapi::shared::minwindef::FILETIME;
+//! use winapi::um::minwinbase::SYSTEMTIME;
+//!
+//! #[derive(Serialize, Deserialize)]
+//! struct SerdeTest {
+//!     #[serde(with = "FileTimeSerde")]
+//!     ft: FILETIME,
+//!     #[serde(with = "SystemTimeSerde")]
+//!     st: SYSTEMTIME,
+//! }
+//! # }
+//! ```
+extern crate comedy;
+#[cfg(feature = "filetime_serde")]
+extern crate serde;
+#[cfg(feature = "filetime_serde")]
+extern crate serde_derive;
+extern crate winapi;
+
+use std::cmp::Ordering;
+use std::fmt::{Debug, Display, Formatter, Result};
+use std::mem;
+use std::result;
+
+use comedy::check_true;
+
+use winapi::shared::minwindef::FILETIME;
+#[cfg(feature = "filetime_serde")]
+use winapi::shared::minwindef::{DWORD, WORD};
+use winapi::shared::ntdef::ULARGE_INTEGER;
+use winapi::um::minwinbase::SYSTEMTIME;
+use winapi::um::sysinfoapi::GetSystemTime;
+use winapi::um::timezoneapi::{FileTimeToSystemTime, SystemTimeToFileTime};
+
+#[cfg(feature = "filetime_serde")]
+use serde_derive::{Deserialize, Serialize};
+
+#[cfg(feature = "filetime_serde")]
+#[allow(non_snake_case)]
+#[derive(Serialize, Deserialize)]
+#[serde(remote = "FILETIME")]
+pub struct FileTimeSerde {
+    dwLowDateTime: DWORD,
+    dwHighDateTime: DWORD,
+}
+
+/// Wraps `FILETIME`
+#[derive(Copy, Clone)]
+#[cfg_attr(feature = "filetime_serde", derive(Serialize, Deserialize))]
+#[repr(transparent)]
+pub struct FileTime(
+    #[cfg_attr(feature = "filetime_serde", serde(with = "FileTimeSerde"))] pub FILETIME,
+);
+
+#[cfg(feature = "filetime_serde")]
+#[allow(non_snake_case)]
+#[derive(Serialize, Deserialize)]
+#[serde(remote = "SYSTEMTIME")]
+pub struct SystemTimeSerde {
+    wYear: WORD,
+    wMonth: WORD,
+    wDayOfWeek: WORD,
+    wDay: WORD,
+    wHour: WORD,
+    wMinute: WORD,
+    wSecond: WORD,
+    wMilliseconds: WORD,
+}
+
+/// Wraps `SYSTEMTIME`
+///
+/// The `SYSTEMTIME` struct can be UTC or local time, but `SystemTimeUTC` should only be used for
+/// UTC.
+///
+#[derive(Copy, Clone)]
+#[cfg_attr(feature = "filetime_serde", derive(Serialize, Deserialize))]
+#[repr(transparent)]
+pub struct SystemTimeUTC(
+    #[cfg_attr(feature = "filetime_serde", serde(with = "SystemTimeSerde"))] pub SYSTEMTIME,
+);
+
+impl FileTime {
+    /// Convert to raw integer
+    ///
+    /// `FILETIME` is 100-nanosecond intervals since January 1, 1601 (UTC), but if the high
+    /// bit is 1 there may be a different interpretation.
+    pub fn to_u64(self) -> u64 {
+        unsafe {
+            let mut v: ULARGE_INTEGER = mem::zeroed();
+            v.s_mut().LowPart = self.0.dwLowDateTime;
+            v.s_mut().HighPart = self.0.dwHighDateTime;
+            *v.QuadPart()
+        }
+    }
+
+    /// Convert to `SystemTimeUTC` via `FileTimeToSystemTime()`
+    pub fn to_system_time_utc(self) -> result::Result<SystemTimeUTC, comedy::Win32Error> {
+        unsafe {
+            let mut system_time = mem::zeroed();
+
+            check_true!(FileTimeToSystemTime(&self.0, &mut system_time))?;
+
+            Ok(SystemTimeUTC(system_time))
+        }
+    }
+}
+
+impl PartialEq for FileTime {
+    fn eq(&self, other: &FileTime) -> bool {
+        self.0.dwLowDateTime == other.0.dwLowDateTime
+            && self.0.dwHighDateTime == other.0.dwHighDateTime
+    }
+}
+
+impl Eq for FileTime {}
+
+impl PartialOrd for FileTime {
+    fn partial_cmp(&self, other: &FileTime) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for FileTime {
+    fn cmp(&self, other: &FileTime) -> Ordering {
+        self.to_u64().cmp(&other.to_u64())
+    }
+}
+
+impl Debug for FileTime {
+    fn fmt(&self, f: &mut Formatter) -> Result {
+        write!(
+            f,
+            "FileTime {{ dwLowDateTime: {:?}, dwHighDateTime: {:?} }}",
+            self.0.dwLowDateTime, self.0.dwHighDateTime
+        )
+    }
+}
+
+impl SystemTimeUTC {
+    /// Get current system time in UTC via `GetSystemTime()`
+    ///
+    /// "Because the system time can be adjusted either forward or backward, do not compare
+    /// system time readings to determine elapsed time."
+    pub fn now() -> SystemTimeUTC {
+        unsafe {
+            let mut system_time = mem::zeroed();
+            GetSystemTime(&mut system_time);
+            SystemTimeUTC(system_time)
+        }
+    }
+
+    /// Convert to `FileTime` via `SystemTimeToFileTime()`
+    pub fn to_file_time(&self) -> result::Result<FileTime, comedy::Win32Error> {
+        unsafe {
+            let mut file_time = mem::zeroed();
+
+            check_true!(SystemTimeToFileTime(&self.0, &mut file_time))?;
+
+            Ok(FileTime(file_time))
+        }
+    }
+}
+
+impl Debug for SystemTimeUTC {
+    fn fmt(&self, f: &mut Formatter) -> Result {
+        write!(
+            f,
+            concat!(
+                "SystemTimeUTC {{ ",
+                "wYear: {:?}, wMonth: {:?}, wDayOfWeek: {:?}, wDay: {:?}, ",
+                "wHour: {:?}, wMinute: {:?}, wSecond: {:?}, wMilliseconds: {:?} }}"
+            ),
+            self.0.wYear,
+            self.0.wMonth,
+            self.0.wDayOfWeek,
+            self.0.wDay,
+            self.0.wHour,
+            self.0.wMinute,
+            self.0.wSecond,
+            self.0.wMilliseconds
+        )
+    }
+}
+
+/// Format as ISO 8601 date and time: `YYYY-MM-DDThh:mm:ss.fffZ`
+impl Display for SystemTimeUTC {
+    fn fmt(&self, f: &mut Formatter) -> Result {
+        write!(
+            f,
+            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
+            self.0.wYear,
+            self.0.wMonth,
+            self.0.wDay,
+            self.0.wHour,
+            self.0.wMinute,
+            self.0.wSecond,
+            self.0.wMilliseconds
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{FileTime, SystemTimeUTC};
+    use winapi::shared::minwindef::FILETIME;
+    use winapi::um::minwinbase::SYSTEMTIME;
+
+    #[test]
+    fn roundtrip() {
+        let ft = SystemTimeUTC::now().to_file_time().unwrap();
+
+        assert_eq!(ft, ft);
+        assert_eq!(ft, ft.to_system_time_utc().unwrap().to_file_time().unwrap());
+    }
+
+    #[test]
+    fn next_year() {
+        let st_now = SystemTimeUTC::now();
+        let st_next_year = SystemTimeUTC(SYSTEMTIME {
+            wYear: st_now.0.wYear + 1,
+            ..st_now.0
+        });
+
+        let ft_now = st_now.to_file_time().unwrap();
+        let ft_next_year = st_next_year.to_file_time().unwrap();
+        assert!(ft_next_year > ft_now);
+    }
+
+    #[test]
+    fn non_time_filetime() {
+        let ft = FileTime(FILETIME {
+            dwLowDateTime: 0xFFFF_FFFFu32,
+            dwHighDateTime: 0xFFFF_FFFFu32,
+        });
+
+        ft.to_system_time_utc().expect_err("should have failed");
+    }
+}
new file mode 100644
--- /dev/null
+++ b/third_party/rust/guid_win/.cargo-checksum.json
@@ -0,0 +1,1 @@
+{"files":{"Cargo.toml":"b9dd70621a7d9c095c7b426965a17b99352ad71d9db5be6e4167b88e01ace4de","LICENSE-APACHE":"b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1","LICENSE-MIT":"b9b0616c8d5ca93cd2a9cce115a2a5a977b1d586b4983afa98a1ec287b9ac623","src/lib.rs":"0ce6cd46f69b40fa4153f54ef5040b733ea3be2a709f4badac3d4d6afe5a072b"},"package":"87261686cc5e35b6584f4c2a430c2b153d8a92ab1ef820c16be34c1df8f5f58b"}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/third_party/rust/guid_win/Cargo.toml
@@ -0,0 +1,40 @@
+# 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 believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+name = "guid_win"
+version = "0.1.0"
+authors = ["Adam Gashlin <agashlin@mozilla.com>"]
+description = "Windows GUID/CLSID/IID string and binary serialization"
+keywords = ["windows", "com", "win32"]
+categories = ["api-bindings", "os::windows-apis"]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/agashlin/guid_win"
+[package.metadata.docs.rs]
+default-target = "x86_64-pc-windows-msvc"
+[dependencies.comedy]
+version = "0.1.0"
+
+[dependencies.serde]
+version = "1.0.80"
+optional = true
+
+[dependencies.serde_derive]
+version = "1.0.80"
+optional = true
+
+[dependencies.winapi]
+version = "0.3.6"
+features = ["combaseapi", "guiddef"]
+
+[features]
+guid_serde = ["serde", "serde_derive"]
new file mode 100644
--- /dev/null
+++ b/third_party/rust/guid_win/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/guid_win/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright (c) 2019 The guid_win Developers
+
+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.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/guid_win/src/lib.rs
@@ -0,0 +1,189 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Windows GUID/CLSID/IID string and binary serialization
+//!
+//! [`Guid`](struct.Guid.html) transparently wraps
+//! [`GUID`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa373931(v=vs.85).aspx).
+//!
+//! Implements `Display` and `FromStr` string conversion, also `Hash` and `Eq`.
+//!
+//! Curly braces (`{}`) are optional for `FromStr`.
+//!
+//! # serde #
+//!
+//! Use the `guid_serde` feature to derive `Serialize` and `Deserialize`, you can then
+//! derive them for structs containing `GUID` like so:
+//!
+//! ```
+//! # fn main() {}
+//! #
+//! # #[cfg(feature = "guid_serde")]
+//! # extern crate serde_derive;
+//! # extern crate winapi;
+//! #
+//! # #[cfg(feature = "guid_serde")]
+//! # mod test {
+//! use guid_win::GUIDSerde;
+//! use serde_derive::{Deserialize, Serialize};
+//! use winapi::shared::guiddef::GUID;
+//!
+//! #[derive(Serialize, Deserialize)]
+//! struct SerdeTest {
+//!     #[serde(with = "GUIDSerde")]
+//!     guid: GUID,
+//! }
+//! # }
+//! ```
+
+extern crate comedy;
+#[cfg(feature = "guid_serde")]
+extern crate serde;
+#[cfg(feature = "guid_serde")]
+extern crate serde_derive;
+extern crate winapi;
+
+use std::ffi::{OsStr, OsString};
+use std::fmt::{Debug, Display, Error, Formatter, Result};
+use std::hash::{Hash, Hasher};
+use std::mem;
+use std::os::windows::ffi::{OsStrExt, OsStringExt};
+use std::result;
+use std::str::FromStr;
+
+use comedy::check_succeeded;
+
+use winapi::ctypes;
+use winapi::shared::guiddef::GUID;
+use winapi::um::combaseapi::{CLSIDFromString, StringFromGUID2};
+
+#[cfg(feature = "guid_serde")]
+use serde_derive::{Deserialize, Serialize};
+
+const GUID_STRING_CHARACTERS: usize = 38;
+
+#[cfg(feature = "guid_serde")]
+#[allow(non_snake_case)]
+#[derive(Serialize, Deserialize)]
+#[serde(remote = "GUID")]
+pub struct GUIDSerde {
+    Data1: ctypes::c_ulong,
+    Data2: ctypes::c_ushort,
+    Data3: ctypes::c_ushort,
+    Data4: [ctypes::c_uchar; 8],
+}
+
+/// Wraps `GUID`
+#[derive(Clone)]
+#[cfg_attr(feature = "guid_serde", derive(Serialize, Deserialize))]
+#[repr(transparent)]
+pub struct Guid(#[cfg_attr(feature = "guid_serde", serde(with = "GUIDSerde"))] pub GUID);
+
+impl PartialEq for Guid {
+    fn eq(&self, other: &Guid) -> bool {
+        self.0.Data1 == other.0.Data1
+            && self.0.Data2 == other.0.Data2
+            && self.0.Data3 == other.0.Data3
+            && self.0.Data4 == other.0.Data4
+    }
+}
+
+impl Eq for Guid {}
+
+impl Hash for Guid {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.0.Data1.hash(state);
+        self.0.Data2.hash(state);
+        self.0.Data3.hash(state);
+        self.0.Data4.hash(state);
+    }
+}
+
+impl Debug for Guid {
+    fn fmt(&self, f: &mut Formatter) -> Result {
+        write!(f, "{:?}", unsafe {
+            &mem::transmute::<Guid, [u8; mem::size_of::<Guid>()]>(self.clone())
+        })
+    }
+}
+
+/// Output a string via `StringFromGUID2()`
+impl Display for Guid {
+    fn fmt(&self, f: &mut Formatter) -> Result {
+        let mut s: [u16; GUID_STRING_CHARACTERS + 1] = unsafe { mem::uninitialized() };
+
+        let len = unsafe {
+            StringFromGUID2(
+                &(*self).0 as *const _ as *mut _,
+                s.as_mut_ptr(),
+                s.len() as ctypes::c_int,
+            )
+        };
+
+        if len <= 0 {
+            return Err(Error);
+        }
+        // len is number of characters, including the null terminator
+
+        let s = &s[..len as usize - 1];
+        // TODO: no reason to expect this to fail, maybe just unwrap()
+        if let Ok(s) = OsString::from_wide(&s).into_string() {
+            f.write_str(&s)
+        } else {
+            Err(Error)
+        }
+    }
+}
+
+/// Read from a string via `CLSIDFromString()`
+///
+/// Braces (`{}`) are added if missing.
+impl FromStr for Guid {
+    type Err = comedy::error::HResult;
+
+    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
+        let mut guid = unsafe { mem::uninitialized() };
+
+        let braced;
+        let s = if s.starts_with('{') {
+            s
+        } else {
+            braced = format!("{{{}}}", s);
+            braced.as_str()
+        };
+        let s: Vec<_> = OsStr::new(s).encode_wide().chain(Some(0)).collect();
+
+        unsafe { check_succeeded!(CLSIDFromString(s.as_ptr(), &mut guid)) }?;
+
+        Ok(Guid(guid))
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::Guid;
+    use std::str::FromStr;
+
+    #[test]
+    fn without_braces() {
+        let uuid = "F1BD1079-9F01-4BDC-8036-F09B70095066";
+        let guid = Guid::from_str(uuid).unwrap();
+        assert_eq!(format!("{}", guid), format!("{{{}}}", uuid));
+    }
+
+    #[test]
+    fn with_braces() {
+        let uuid = "{F1BD1079-9F01-4BDC-8036-F09B70095066}";
+        let guid = Guid::from_str(uuid).unwrap();
+        assert_eq!(format!("{}", guid), uuid);
+    }
+
+    #[test]
+    fn format_error() {
+        let uuid = "foo";
+        Guid::from_str(uuid).unwrap_err();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "bitsdownload"
+version = "0.1.0"
+authors = ["nobody@mozilla.org"]
+license = "MPL-2.0"
+
+[dependencies]
+bits_client = { path = "./bits_client" }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/.gitignore
@@ -0,0 +1,3 @@
+/target
+**/*.rs.bk
+**/.*.swp
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "bits_client"
+version = "0.1.0"
+authors = ["Adam Gashlin <agashlin@mozilla.com>"]
+license = "MPL-2.0"
+publish = false
+
+[dependencies]
+bits = { path = "./bits" }
+comedy = "0.1.0"
+guid_win = "0.1.0"
+
+[dependencies.failure]
+version = "0.1.3"
+features = ["derive"]
+default_features = false
+
+[dependencies.failure_derive]
+version = "0.1.3"
+
+[dev-dependencies]
+#ctrlc = "3.1.1"
+lazy_static = "1.0.1"
+rand = "0.4.3"
+regex = "1"
+tempdir = "0.3.5"
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/README.md
@@ -0,0 +1,33 @@
+bits\_client
+============
+
+Interfaces for BITS.
+
+building
+--------
+
+This relies on a few things that are not yet in a released `winapi`, you can use the following patch in `Cargo.toml`:
+
+```toml
+[patch.crates-io]
+winapi = { git = "https://github.com/froydnj/winapi-rs", branch = "aarch64" }
+```
+
+bits\_client lib
+---------------
+
+`bits_client` is the primary target and provides `BitsClient`, an API for creating and monitoring BITS jobs.
+
+`bits_client::new()` creates a `BitsClient` that does all operations within the current process, as the current user.
+
+bits crate
+----------
+
+`bits` is a safe interface to BITS, providing connections to the
+Background Copy Manager, some basic operations on Background Copy Jobs, and
+methods for implementing `IBackgroundCopyCallback`s in Rust.
+
+test\_client example
+-------------------
+
+`examples/test_client.rs` shows how to use the API.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "bits"
+version = "0.1.0"
+authors = ["Adam Gashlin <agashlin@mozilla.com>"]
+license = "MIT/Apache-2.0"
+publish = false
+
+[features]
+status_serde = ["serde", "serde_derive"]
+
+[dependencies]
+comedy = "0.1.0"
+filetime_win = "0.1.0"
+guid_win = "0.1.0"
+serde = { version = "1.0.80", optional = true }
+serde_derive = { version = "1.0.80", optional = true }
+
+[dependencies.winapi]
+version = "0.3.6"
+features = ["basetsd",
+            "bits",
+            "bits2_5",
+            "bitsmsg",
+            "guiddef",
+            "minwindef",
+            "ntdef",
+            "rpcndr",
+            "unknwnbase",
+            "winerror",
+            "winnls",
+            ]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/callback.rs
@@ -0,0 +1,205 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+use std::panic::{catch_unwind, RefUnwindSafe};
+use std::ptr::NonNull;
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use comedy::{com::ComRef, HResult};
+use guid_win::Guid;
+use winapi::ctypes::c_void;
+use winapi::shared::guiddef::REFIID;
+use winapi::shared::minwindef::DWORD;
+use winapi::shared::ntdef::ULONG;
+use winapi::shared::winerror::{E_FAIL, E_NOINTERFACE, HRESULT, NOERROR, S_OK};
+use winapi::um::bits::{
+    IBackgroundCopyCallback, IBackgroundCopyCallbackVtbl, IBackgroundCopyError, IBackgroundCopyJob,
+};
+use winapi::um::unknwnbase::{IUnknown, IUnknownVtbl};
+use winapi::Interface;
+
+use BitsJob;
+
+/// The type of a notification callback.
+///
+/// The callbacks must be `Fn()` to be called arbitrarily many times, `RefUnwindSafe` to have a
+/// panic unwind safely caught, `Send`, `Sync` and `'static` to run on any thread COM invokes us on
+/// any time.
+///
+/// If the callback returns a non-success `HRESULT`, the notification may pass to other BITS
+/// mechanisms such as `IBackgroundCopyJob2::SetNotifyCmdLine`.
+pub type TransferredCallback =
+    (Fn() -> Result<(), HRESULT>) + RefUnwindSafe + Send + Sync + 'static;
+pub type ErrorCallback = (Fn() -> Result<(), HRESULT>) + RefUnwindSafe + Send + Sync + 'static;
+pub type ModificationCallback =
+    (Fn() -> Result<(), HRESULT>) + RefUnwindSafe + Send + Sync + 'static;
+
+#[repr(C)]
+pub struct BackgroundCopyCallback {
+    // Everything assumes that the interface vtable is the first member of this struct.
+    interface: IBackgroundCopyCallback,
+    rc: AtomicUsize,
+    transferred_cb: Option<Box<TransferredCallback>>,
+    error_cb: Option<Box<ErrorCallback>>,
+    modification_cb: Option<Box<ModificationCallback>>,
+}
+
+impl BackgroundCopyCallback {
+    /// Construct the callback object and register it with a job.
+    ///
+    /// Only one notify interface can be present on a job at once, so this will release BITS'
+    /// ref to any previously registered interface.
+    pub fn register(
+        job: &mut BitsJob,
+        transferred_cb: Option<Box<TransferredCallback>>,
+        error_cb: Option<Box<ErrorCallback>>,
+        modification_cb: Option<Box<ModificationCallback>>,
+    ) -> Result<(), HResult> {
+        let cb = Box::new(BackgroundCopyCallback {
+            interface: IBackgroundCopyCallback { lpVtbl: &VTBL },
+            rc: AtomicUsize::new(1),
+            transferred_cb,
+            error_cb,
+            modification_cb,
+        });
+
+        // Leak the callback, it has no Rust owner until we need to drop it later.
+        // The ComRef will Release when it goes out of scope.
+        unsafe {
+            let cb = ComRef::from_raw(NonNull::new_unchecked(Box::into_raw(cb) as *mut IUnknown));
+
+            job.set_notify_interface(cb.as_raw_ptr())?;
+        }
+
+        Ok(())
+    }
+}
+
+extern "system" fn query_interface(
+    this: *mut IUnknown,
+    riid: REFIID,
+    obj: *mut *mut c_void,
+) -> HRESULT {
+    unsafe {
+        // `IBackgroundCopyCallback` is the first (currently only) interface on the
+        // `BackgroundCopyCallback` object, so we can return `this` either as
+        // `IUnknown` or `IBackgroundCopyCallback`.
+        if Guid(*riid) == Guid(IUnknown::uuidof())
+            || Guid(*riid) == Guid(IBackgroundCopyCallback::uuidof())
+        {
+            addref(this);
+            // Cast first to `IBackgroundCopyCallback` to be clear which `IUnknown`
+            // we are pointing at.
+            *obj = this as *mut IBackgroundCopyCallback as *mut c_void;
+            NOERROR
+        } else {
+            E_NOINTERFACE
+        }
+    }
+}
+
+extern "system" fn addref(raw_this: *mut IUnknown) -> ULONG {
+    unsafe {
+        let this = raw_this as *const BackgroundCopyCallback;
+
+        // Forge a reference for just this statement.
+        let old_rc = (*this).rc.fetch_add(1, Ordering::SeqCst);
+        (old_rc + 1) as ULONG
+    }
+}
+
+extern "system" fn release(raw_this: *mut IUnknown) -> ULONG {
+    unsafe {
+        {
+            let this = raw_this as *const BackgroundCopyCallback;
+
+            // Forge a reference for just this statement.
+            let old_rc = (*this).rc.fetch_sub(1, Ordering::SeqCst);
+
+            let rc = old_rc - 1;
+            if rc > 0 {
+                return rc as ULONG;
+            }
+        }
+
+        // rc will have been 0 for us to get here, and we're out of scope of the reference above,
+        // so there should be no references or pointers left (besides `this`).
+        // Re-Box and to drop immediately.
+        let _ = Box::from_raw(raw_this as *mut BackgroundCopyCallback);
+
+        0
+    }
+}
+
+extern "system" fn transferred_stub(
+    raw_this: *mut IBackgroundCopyCallback,
+    _job: *mut IBackgroundCopyJob,
+) -> HRESULT {
+    unsafe {
+        let this = raw_this as *const BackgroundCopyCallback;
+        // Forge a reference just for this statement.
+        if let Some(ref cb) = (*this).transferred_cb {
+            match catch_unwind(|| cb()) {
+                Ok(Ok(())) => S_OK,
+                Ok(Err(hr)) => hr,
+                Err(_) => E_FAIL,
+            }
+        } else {
+            S_OK
+        }
+    }
+}
+
+extern "system" fn error_stub(
+    raw_this: *mut IBackgroundCopyCallback,
+    _job: *mut IBackgroundCopyJob,
+    _error: *mut IBackgroundCopyError,
+) -> HRESULT {
+    unsafe {
+        let this = raw_this as *const BackgroundCopyCallback;
+        // Forge a reference just for this statement.
+        if let Some(ref cb) = (*this).error_cb {
+            match catch_unwind(|| cb()) {
+                Ok(Ok(())) => S_OK,
+                Ok(Err(hr)) => hr,
+                Err(_) => E_FAIL,
+            }
+        } else {
+            S_OK
+        }
+    }
+}
+
+extern "system" fn modification_stub(
+    raw_this: *mut IBackgroundCopyCallback,
+    _job: *mut IBackgroundCopyJob,
+    _reserved: DWORD,
+) -> HRESULT {
+    unsafe {
+        let this = raw_this as *const BackgroundCopyCallback;
+        // Forge a reference just for this statement.
+        if let Some(ref cb) = (*this).modification_cb {
+            match catch_unwind(|| cb()) {
+                Ok(Ok(())) => S_OK,
+                Ok(Err(hr)) => hr,
+                Err(_) => E_FAIL,
+            }
+        } else {
+            S_OK
+        }
+    }
+}
+
+pub static VTBL: IBackgroundCopyCallbackVtbl = IBackgroundCopyCallbackVtbl {
+    parent: IUnknownVtbl {
+        QueryInterface: query_interface,
+        AddRef: addref,
+        Release: release,
+    },
+    JobTransferred: transferred_stub,
+    JobError: error_stub,
+    JobModification: modification_stub,
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/lib.rs
@@ -0,0 +1,579 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! A safe interface for BITS
+//!
+//! The primary entry point into BITS is the
+//! [`BackgroundCopyManager`](struct.BackgroundCopyManager.html) struct.
+//!
+//! Functionality is only provided by this crate on an as-needed basis for
+//! [bits_client](../bits_client/index.html), so there are vast swathes of the BITS API
+//! unsupported.
+
+extern crate comedy;
+extern crate filetime_win;
+extern crate guid_win;
+extern crate winapi;
+
+#[cfg(feature = "status_serde")]
+extern crate serde;
+#[cfg(feature = "status_serde")]
+extern crate serde_derive;
+
+mod callback;
+pub mod status;
+mod wide;
+
+use std::ffi::{OsStr, OsString};
+use std::mem;
+use std::os::windows::ffi::OsStringExt;
+use std::ptr;
+use std::result;
+
+use comedy::com::{create_instance_local_server, CoTaskMem, ComRef, INIT_MTA};
+use comedy::error::{HResult, ResultExt};
+use comedy::{com_call, com_call_getter, com_call_taskmem_getter};
+use filetime_win::FileTime;
+use guid_win::Guid;
+use winapi::shared::minwindef::DWORD;
+use winapi::shared::ntdef::{HRESULT, LANGIDFROMLCID, ULONG};
+use winapi::shared::winerror::S_FALSE;
+use winapi::um::bits::{
+    IBackgroundCopyError, IBackgroundCopyFile, IBackgroundCopyJob, IBackgroundCopyManager,
+    IEnumBackgroundCopyFiles, IEnumBackgroundCopyJobs, BG_JOB_PRIORITY, BG_JOB_PRIORITY_FOREGROUND,
+    BG_JOB_PRIORITY_HIGH, BG_JOB_PRIORITY_LOW, BG_JOB_PRIORITY_NORMAL, BG_JOB_PROXY_USAGE,
+    BG_JOB_PROXY_USAGE_AUTODETECT, BG_JOB_PROXY_USAGE_NO_PROXY, BG_JOB_PROXY_USAGE_PRECONFIG,
+    BG_JOB_STATE_ERROR, BG_JOB_STATE_TRANSIENT_ERROR, BG_JOB_TYPE_DOWNLOAD, BG_NOTIFY_DISABLE,
+    BG_NOTIFY_JOB_ERROR, BG_NOTIFY_JOB_MODIFICATION, BG_NOTIFY_JOB_TRANSFERRED, BG_SIZE_UNKNOWN,
+};
+use winapi::um::bits2_5::{IBackgroundCopyJobHttpOptions, BG_HTTP_REDIRECT_POLICY_ALLOW_REPORT};
+use winapi::um::bitsmsg::BG_E_NOT_FOUND;
+use winapi::um::unknwnbase::IUnknown;
+use winapi::um::winnls::GetThreadLocale;
+
+pub use winapi::um::bits::{BG_ERROR_CONTEXT, BG_JOB_STATE};
+pub use winapi::um::bitsmsg::{BG_S_PARTIAL_COMPLETE, BG_S_UNABLE_TO_DELETE_FILES};
+
+pub use status::{
+    BitsErrorContext, BitsJobError, BitsJobProgress, BitsJobState, BitsJobStatus, BitsJobTimes,
+};
+use wide::ToWideNull;
+
+pub use winapi::shared::winerror::E_FAIL;
+
+#[repr(u32)]
+#[derive(Copy, Clone, Debug)]
+pub enum BitsJobPriority {
+    Foreground = BG_JOB_PRIORITY_FOREGROUND,
+    High = BG_JOB_PRIORITY_HIGH,
+    /// Default
+    Normal = BG_JOB_PRIORITY_NORMAL,
+    Low = BG_JOB_PRIORITY_LOW,
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, Debug)]
+pub enum BitsProxyUsage {
+    /// Directly access the network.
+    NoProxy = BG_JOB_PROXY_USAGE_NO_PROXY,
+    /// Use Internet Explorer proxy settings. This is the default.
+    Preconfig = BG_JOB_PROXY_USAGE_PRECONFIG,
+    /// Attempt to auto-detect the connection's proxy settings.
+    AutoDetect = BG_JOB_PROXY_USAGE_AUTODETECT,
+}
+
+type Result<T> = result::Result<T, HResult>;
+
+pub struct BackgroundCopyManager(ComRef<IBackgroundCopyManager>);
+
+impl BackgroundCopyManager {
+    /// Get access to the local BITS service.
+    ///
+    /// # COM Initialization and Threading Model #
+    ///
+    /// This method uses a thread local variable to initialize COM with a multithreaded apartment
+    /// model for this thread, and leaves it this way until the thread local is dropped.
+    /// If the thread was in a single-threaded apartment, `connect()` will fail gracefully.
+    ///
+    /// # Safety #
+    ///
+    /// If there are mismatched `CoUninitialize` calls on this thread which lead to COM shutting
+    /// down before this thread ends, unsafe behavior may result.
+    pub fn connect() -> Result<BackgroundCopyManager> {
+        INIT_MTA.with(|com| {
+            if let Err(e) = com {
+                return Err(e.clone());
+            }
+            Ok(())
+        })?;
+
+        // Assuming no mismatched CoUninitialize calls, methods do not have to check for
+        // successfully initialized COM once the object is constructed: `BackgroundCopyManager`
+        // is not `Send` or `Sync` so it must be used on the thread it was constructed on,
+        // which has now successfully inited MTA for the lifetime of thread local `INIT_MTA`.
+        // This also holds for any functions using pointers only derived from these methods, like
+        // the `BitsJob` methods.
+
+        Ok(BackgroundCopyManager(create_instance_local_server::<
+            winapi::um::bits::BackgroundCopyManager,
+            IBackgroundCopyManager,
+        >()?))
+    }
+
+    /// Create a new download job with the given name.
+    pub fn create_job(&self, display_name: &OsStr) -> Result<BitsJob> {
+        unsafe {
+            let mut guid = mem::zeroed();
+            Ok(BitsJob(com_call_getter!(
+                |job| self.0,
+                IBackgroundCopyManager::CreateJob(
+                    display_name.to_wide_null().as_ptr(),
+                    BG_JOB_TYPE_DOWNLOAD,
+                    &mut guid,
+                    job,
+                )
+            )?))
+        }
+    }
+
+    /// Cancel all jobs with the given name.
+    ///
+    /// This only attempts to cancel jobs owned by the current user.
+    /// No errors are returned for jobs that failed to cancel.
+    pub fn cancel_jobs_by_name(&self, match_name: &OsStr) -> Result<()> {
+        let jobs =
+            unsafe { com_call_getter!(|jobs| self.0, IBackgroundCopyManager::EnumJobs(0, jobs))? };
+
+        loop {
+            let result = unsafe {
+                com_call_getter!(
+                    |job| jobs,
+                    IEnumBackgroundCopyJobs::Next(1, job, ptr::null_mut())
+                )
+            };
+            match result {
+                Ok(job) => {
+                    if job_name_eq(&job, match_name)? {
+                        unsafe {
+                            let _ = com_call!(job, IBackgroundCopyJob::Cancel());
+                        }
+                    }
+                }
+                Err(e) => {
+                    if e.code() == S_FALSE {
+                        // Ran out of jobs to enumerate
+                        return Ok(());
+                    } else {
+                        return Err(e);
+                    }
+                }
+            }
+        }
+    }
+
+    /// Get the job with the given GUID.
+    ///
+    /// Returns Err if the job was not found.
+    pub fn get_job_by_guid(&self, guid: &Guid) -> Result<BitsJob> {
+        unsafe { com_call_getter!(|job| self.0, IBackgroundCopyManager::GetJob(&guid.0, job)) }
+            .map(BitsJob)
+    }
+
+    /// Try to find a job with a given GUID.
+    ///
+    /// Returns Ok(None) if the job was not found but there was no other error.
+    pub fn find_job_by_guid(&self, guid: &Guid) -> Result<Option<BitsJob>> {
+        Ok(self
+            .get_job_by_guid(guid)
+            .map(Some)
+            .allow_err(BG_E_NOT_FOUND as i32, None)?)
+    }
+
+    /// Try to find a job with a given GUID and name.
+    ///
+    /// Returns Ok(None) if the job was not found, or if it had the wrong name, as long as there
+    /// was no other error.
+    pub fn find_job_by_guid_and_name(
+        &self,
+        guid: &Guid,
+        match_name: &OsStr,
+    ) -> Result<Option<BitsJob>> {
+        Ok(match self.find_job_by_guid(guid)? {
+            None => None,
+            Some(BitsJob(ref job)) if !job_name_eq(job, match_name)? => None,
+            result => result,
+        })
+    }
+
+    /// Translate a BITS `HRESULT` to a textual description.
+    ///
+    /// This uses the current thread's locale to look up the message associated with a BITS
+    /// error. It should only be used for `HRESULT`s returned from BITS COM interfaces.
+    pub fn get_error_description(&self, hr: HRESULT) -> Result<String> {
+        unsafe {
+            let language_id = DWORD::from(LANGIDFROMLCID(GetThreadLocale()));
+
+            Ok(taskmem_into_lossy_string(com_call_taskmem_getter!(
+                |desc| self.0,
+                IBackgroundCopyManager::GetErrorDescription(hr, language_id, desc)
+            )?))
+        }
+    }
+}
+
+unsafe fn taskmem_into_lossy_string(taskmem: CoTaskMem<u16>) -> String {
+    OsString::from_wide(taskmem.as_slice_until_null())
+        .to_string_lossy()
+        .into_owned()
+}
+
+fn job_name_eq(job: &ComRef<IBackgroundCopyJob>, match_name: &OsStr) -> Result<bool> {
+    let job_name = unsafe {
+        OsString::from_wide(
+            com_call_taskmem_getter!(|name| job, IBackgroundCopyJob::GetDisplayName(name))?
+                .as_slice_until_null(),
+        )
+    };
+
+    Ok(job_name == match_name)
+}
+
+pub struct BitsJob(ComRef<IBackgroundCopyJob>);
+
+impl BitsJob {
+    /// Get the job's GUID.
+    pub fn guid(&self) -> Result<Guid> {
+        // TODO: cache on create or retrieved by GUID?
+        unsafe {
+            let mut guid = mem::zeroed();
+            com_call!(self.0, IBackgroundCopyJob::GetId(&mut guid))?;
+            Ok(Guid(guid))
+        }
+    }
+
+    /// Add a file to the job.
+    pub fn add_file(&mut self, remote_url: &OsStr, local_file: &OsStr) -> Result<()> {
+        unsafe {
+            com_call!(
+                self.0,
+                IBackgroundCopyJob::AddFile(
+                    remote_url.to_wide_null().as_ptr(),
+                    local_file.to_wide_null().as_ptr(),
+                )
+            )
+        }?;
+        Ok(())
+    }
+
+    /// Get the first file in the job.
+    ///
+    /// This is provided for collecting the redirected remote name of single file jobs.
+    pub fn get_first_file(&mut self) -> Result<BitsFile> {
+        let files = unsafe { com_call_getter!(|e| self.0, IBackgroundCopyJob::EnumFiles(e))? };
+
+        let file = unsafe {
+            com_call_getter!(
+                |file| files,
+                IEnumBackgroundCopyFiles::Next(1, file, ptr::null_mut())
+            )?
+        };
+
+        Ok(BitsFile(file))
+    }
+
+    /// Set the job's description string.
+    ///
+    /// This is different from the display name set when creating the job.
+    pub fn set_description(&mut self, description: &OsStr) -> Result<()> {
+        unsafe {
+            com_call!(
+                self.0,
+                IBackgroundCopyJob::SetDescription(description.to_wide_null().as_ptr())
+            )
+        }?;
+        Ok(())
+    }
+
+    /// Change the job's proxy usage setting.
+    ///
+    /// The default is `BitsProxyUsage::Preconfig`.
+    pub fn set_proxy_usage(&mut self, usage: BitsProxyUsage) -> Result<()> {
+        use BitsProxyUsage::*;
+
+        match usage {
+            Preconfig | NoProxy | AutoDetect => {
+                unsafe {
+                    com_call!(
+                        self.0,
+                        IBackgroundCopyJob::SetProxySettings(
+                            usage as BG_JOB_PROXY_USAGE,
+                            ptr::null_mut(),
+                            ptr::null_mut(),
+                        )
+                    )
+                }?;
+                Ok(())
+            }
+        }
+    }
+
+    /// Change the job's priority.
+    ///
+    /// The default is `BitsJobPriority::Normal`.
+    pub fn set_priority(&mut self, priority: BitsJobPriority) -> Result<()> {
+        unsafe {
+            com_call!(
+                self.0,
+                IBackgroundCopyJob::SetPriority(priority as BG_JOB_PRIORITY)
+            )
+        }?;
+        Ok(())
+    }
+
+    pub fn set_minimum_retry_delay(&mut self, seconds: ULONG) -> Result<()> {
+        unsafe { com_call!(self.0, IBackgroundCopyJob::SetMinimumRetryDelay(seconds)) }?;
+        Ok(())
+    }
+
+    /// Enable HTTP redirect reporting.
+    ///
+    /// The default setting is to allow HTTP redirects, but to not report them in any way. With
+    /// this setting enabled, the remote name of a file will be updated to reflect the redirect.
+    ///
+    /// # Compatibility #
+    ///
+    /// First available in Windows Vista.
+    pub fn set_redirect_report(&mut self) -> Result<()> {
+        unsafe {
+            com_call!(
+                self.0.cast()?,
+                IBackgroundCopyJobHttpOptions::SetSecurityFlags(
+                    BG_HTTP_REDIRECT_POLICY_ALLOW_REPORT
+                )
+            )
+        }?;
+
+        Ok(())
+    }
+
+    /// Resume the job. This must be done at least once to initially enqueue the job.
+    pub fn resume(&mut self) -> Result<()> {
+        unsafe { com_call!(self.0, IBackgroundCopyJob::Resume()) }?;
+        Ok(())
+    }
+
+    pub fn suspend(&mut self) -> Result<()> {
+        unsafe { com_call!(self.0, IBackgroundCopyJob::Suspend()) }?;
+        Ok(())
+    }
+
+    /// Complete the job, moving the local files to their final names.
+    ///
+    /// Has two interesting success `HRESULT`s: `BG_S_PARTIAL_COMPLETE` and
+    /// `BG_S_UNABLE_TO_DELETE_FILES`.
+    pub fn complete(&mut self) -> Result<HRESULT> {
+        unsafe { com_call!(self.0, IBackgroundCopyJob::Complete()) }
+    }
+
+    /// Cancel the job, deleting any temporary files.
+    ///
+    /// Has an interesting success `HRESULT`: `BG_S_UNABLE_TO_DELETE_FILES`.
+    pub fn cancel(&mut self) -> Result<HRESULT> {
+        unsafe { com_call!(self.0, IBackgroundCopyJob::Cancel()) }
+    }
+
+    /// Set the notification callbacks to use with this job.
+    ///
+    /// This will replace any previously set callbacks.
+    pub fn register_callbacks(
+        &mut self,
+        transferred_cb: Option<Box<callback::TransferredCallback>>,
+        error_cb: Option<Box<callback::ErrorCallback>>,
+        modification_cb: Option<Box<callback::ModificationCallback>>,
+    ) -> Result<()> {
+        let mut flags = 0;
+        if transferred_cb.is_some() {
+            flags |= BG_NOTIFY_JOB_TRANSFERRED;
+        }
+        if error_cb.is_some() {
+            flags |= BG_NOTIFY_JOB_ERROR;
+        }
+        if modification_cb.is_some() {
+            flags |= BG_NOTIFY_JOB_MODIFICATION;
+        }
+
+        callback::BackgroundCopyCallback::register(
+            self,
+            transferred_cb,
+            error_cb,
+            modification_cb,
+        )?;
+
+        unsafe { com_call!(self.0, IBackgroundCopyJob::SetNotifyFlags(flags)) }?;
+
+        Ok(())
+    }
+
+    fn _clear_callbacks(&mut self) -> Result<()> {
+        unsafe {
+            com_call!(
+                self.0,
+                IBackgroundCopyJob::SetNotifyFlags(BG_NOTIFY_DISABLE)
+            )?;
+
+            self.set_notify_interface(ptr::null_mut() as *mut IUnknown)
+        }
+    }
+
+    /// Collect the current status of the job, including errors.
+    pub fn get_status(&self) -> Result<BitsJobStatus> {
+        let mut state = 0;
+        let mut progress = unsafe { mem::zeroed() };
+        let mut error_count = 0;
+        let mut times = unsafe { mem::zeroed() };
+
+        unsafe {
+            com_call!(self.0, IBackgroundCopyJob::GetState(&mut state))?;
+            com_call!(self.0, IBackgroundCopyJob::GetProgress(&mut progress))?;
+            com_call!(self.0, IBackgroundCopyJob::GetErrorCount(&mut error_count))?;
+            com_call!(self.0, IBackgroundCopyJob::GetTimes(&mut times))?;
+        }
+
+        Ok(BitsJobStatus {
+            state: BitsJobState::from(state),
+            progress: BitsJobProgress {
+                total_bytes: if progress.BytesTotal == BG_SIZE_UNKNOWN {
+                    None
+                } else {
+                    Some(progress.BytesTotal)
+                },
+                transferred_bytes: progress.BytesTransferred,
+                total_files: progress.FilesTotal,
+                transferred_files: progress.FilesTransferred,
+            },
+            error_count,
+            error: if state == BG_JOB_STATE_ERROR || state == BG_JOB_STATE_TRANSIENT_ERROR {
+                let error_obj =
+                    unsafe { com_call_getter!(|e| self.0, IBackgroundCopyJob::GetError(e)) }?;
+
+                Some(BitsJob::get_error(error_obj)?)
+            } else {
+                None
+            },
+            times: BitsJobTimes {
+                creation: FileTime(times.CreationTime),
+                modification: FileTime(times.ModificationTime),
+                transfer_completion: if times.TransferCompletionTime.dwLowDateTime == 0
+                    && times.TransferCompletionTime.dwHighDateTime == 0
+                {
+                    None
+                } else {
+                    Some(FileTime(times.TransferCompletionTime))
+                },
+            },
+        })
+    }
+
+    fn get_error(error_obj: ComRef<IBackgroundCopyError>) -> Result<BitsJobError> {
+        let mut context = 0;
+        let mut hresult = 0;
+        unsafe {
+            com_call!(
+                error_obj,
+                IBackgroundCopyError::GetError(&mut context, &mut hresult)
+            )?;
+
+            let language_id = DWORD::from(LANGIDFROMLCID(GetThreadLocale()));
+
+            Ok(BitsJobError {
+                context: BitsErrorContext::from(context),
+                context_str: taskmem_into_lossy_string(com_call_taskmem_getter!(
+                    |desc| error_obj,
+                    IBackgroundCopyError::GetErrorContextDescription(language_id, desc)
+                )?),
+                error: hresult,
+                error_str: taskmem_into_lossy_string(com_call_taskmem_getter!(
+                    |desc| error_obj,
+                    IBackgroundCopyError::GetErrorDescription(language_id, desc)
+                )?),
+            })
+        }
+    }
+
+    unsafe fn set_notify_interface(&self, interface: *mut IUnknown) -> Result<()> {
+        com_call!(self.0, IBackgroundCopyJob::SetNotifyInterface(interface))?;
+        Ok(())
+    }
+}
+
+pub struct BitsFile(ComRef<IBackgroundCopyFile>);
+
+/// A single file in a BITS job.
+///
+/// This is provided for collecting the redirected remote name.
+impl BitsFile {
+    /// Get the remote name from which the file is being downloaded.
+    ///
+    /// If [`BitsJob::set_redirect_report()`](struct.BitsJob.html#method.set_redirect_report)
+    /// hasn't been called on the job, this won't be
+    /// updated as HTTP redirects are processed.
+    pub fn get_remote_name(&self) -> Result<OsString> {
+        unsafe {
+            Ok(OsString::from_wide(
+                com_call_taskmem_getter!(|name| self.0, IBackgroundCopyFile::GetRemoteName(name))?
+                    .as_slice_until_null(),
+            ))
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::BackgroundCopyManager;
+    use std::ffi::OsString;
+    use std::mem;
+
+    #[test]
+    #[ignore]
+    fn test_find_job() {
+        let bcm = BackgroundCopyManager::connect().unwrap();
+        let name = OsString::from("bits test job");
+        let wrong_name = OsString::from("bits test jobbo");
+
+        let mut job = bcm.create_job(&name).unwrap();
+        let guid = job.guid().unwrap();
+
+        assert_eq!(
+            bcm.find_job_by_guid(&guid)
+                .unwrap()
+                .unwrap()
+                .guid()
+                .unwrap(),
+            guid
+        );
+        assert_eq!(
+            bcm.find_job_by_guid_and_name(&guid, &name)
+                .unwrap()
+                .unwrap()
+                .guid()
+                .unwrap(),
+            guid
+        );
+        assert!(bcm
+            .find_job_by_guid_and_name(&guid, &wrong_name)
+            .unwrap()
+            .is_none());
+
+        job.cancel().unwrap();
+        mem::drop(job);
+
+        assert!(bcm.find_job_by_guid(&guid).unwrap().is_none());
+        assert!(bcm
+            .find_job_by_guid_and_name(&guid, &name)
+            .unwrap()
+            .is_none());
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/status.rs
@@ -0,0 +1,118 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Data types for reporting a job's status
+
+use filetime_win::FileTime;
+use winapi::shared::winerror::HRESULT;
+use winapi::um::bits::{BG_ERROR_CONTEXT, BG_JOB_STATE};
+
+#[cfg(feature = "status_serde")]
+use serde_derive::{Deserialize, Serialize};
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobStatus {
+    pub state: BitsJobState,
+    pub progress: BitsJobProgress,
+    pub error_count: u32,
+    pub error: Option<BitsJobError>,
+    pub times: BitsJobTimes,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobError {
+    pub context: BitsErrorContext,
+    pub context_str: String,
+    pub error: HRESULT,
+    pub error_str: String,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub enum BitsErrorContext {
+    None,
+    Unknown,
+    GeneralQueueManager,
+    QueueManagerNotification,
+    LocalFile,
+    RemoteFile,
+    GeneralTransport,
+    RemoteApplication,
+    /// No other values are documented
+    Other(BG_ERROR_CONTEXT),
+}
+
+impl From<BG_ERROR_CONTEXT> for BitsErrorContext {
+    fn from(ec: BG_ERROR_CONTEXT) -> BitsErrorContext {
+        use self::BitsErrorContext::*;
+        use winapi::um::bits;
+        match ec {
+            bits::BG_ERROR_CONTEXT_NONE => None,
+            bits::BG_ERROR_CONTEXT_UNKNOWN => Unknown,
+            bits::BG_ERROR_CONTEXT_GENERAL_QUEUE_MANAGER => GeneralQueueManager,
+            bits::BG_ERROR_CONTEXT_QUEUE_MANAGER_NOTIFICATION => QueueManagerNotification,
+            bits::BG_ERROR_CONTEXT_LOCAL_FILE => LocalFile,
+            bits::BG_ERROR_CONTEXT_REMOTE_FILE => RemoteFile,
+            bits::BG_ERROR_CONTEXT_GENERAL_TRANSPORT => GeneralTransport,
+            bits::BG_ERROR_CONTEXT_REMOTE_APPLICATION => RemoteApplication,
+            ec => Other(ec),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub enum BitsJobState {
+    Queued,
+    Connecting,
+    Transferring,
+    Suspended,
+    Error,
+    TransientError,
+    Transferred,
+    Acknowledged,
+    Cancelled,
+    /// No other values are documented
+    Other(BG_JOB_STATE),
+}
+
+impl From<BG_JOB_STATE> for BitsJobState {
+    fn from(s: BG_JOB_STATE) -> BitsJobState {
+        use self::BitsJobState::*;
+        use winapi::um::bits;
+        match s {
+            bits::BG_JOB_STATE_QUEUED => Queued,
+            bits::BG_JOB_STATE_CONNECTING => Connecting,
+            bits::BG_JOB_STATE_TRANSFERRING => Transferring,
+            bits::BG_JOB_STATE_SUSPENDED => Suspended,
+            bits::BG_JOB_STATE_ERROR => Error,
+            bits::BG_JOB_STATE_TRANSIENT_ERROR => TransientError,
+            bits::BG_JOB_STATE_TRANSFERRED => Transferred,
+            bits::BG_JOB_STATE_ACKNOWLEDGED => Acknowledged,
+            bits::BG_JOB_STATE_CANCELLED => Cancelled,
+            s => Other(s),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobProgress {
+    pub total_bytes: Option<u64>,
+    pub transferred_bytes: u64,
+    pub total_files: u32,
+    pub transferred_files: u32,
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobTimes {
+    pub creation: FileTime,
+    pub modification: FileTime,
+    pub transfer_completion: Option<FileTime>,
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs
@@ -0,0 +1,38 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+// Minimal null-terminated wide string support from wio.
+
+use std::ffi::{OsStr, OsString};
+use std::os::windows::ffi::{OsStrExt, OsStringExt};
+use std::slice;
+
+pub trait ToWideNull {
+    fn to_wide_null(&self) -> Vec<u16>;
+}
+
+impl<T: AsRef<OsStr>> ToWideNull for T {
+    fn to_wide_null(&self) -> Vec<u16> {
+        self.as_ref().encode_wide().chain(Some(0)).collect()
+    }
+}
+
+pub trait FromWidePtrNull {
+    unsafe fn from_wide_ptr_null(wide: *const u16) -> Self;
+}
+
+impl FromWidePtrNull for OsString {
+    unsafe fn from_wide_ptr_null(wide: *const u16) -> Self {
+        assert!(!wide.is_null());
+
+        for i in 0.. {
+            if *wide.offset(i) == 0 {
+                return Self::from_wide(&slice::from_raw_parts(wide, i as usize));
+            }
+        }
+        unreachable!()
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/examples/test_client.rs
@@ -0,0 +1,305 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+extern crate bits_client;
+extern crate comedy;
+//extern crate ctrlc;
+extern crate failure;
+extern crate failure_derive;
+extern crate guid_win;
+
+use std::convert;
+use std::env;
+use std::ffi::{OsStr, OsString};
+use std::process;
+use std::str::FromStr;
+use std::sync::{Arc, Mutex};
+
+use failure::{AsFail, Fail};
+
+use bits_client::bits_protocol::HResultMessage;
+use bits_client::{BitsClient, BitsJobState, BitsMonitorClient, BitsProxyUsage, Guid, PipeError};
+
+#[derive(Debug, Fail)]
+enum MyError {
+    #[fail(display = "{}", _0)]
+    Msg(String),
+    #[fail(display = "HResult")]
+    HResult(#[fail(cause)] comedy::HResult),
+    #[fail(display = "Win32Error")]
+    Win32Error(#[fail(cause)] comedy::Win32Error),
+    #[fail(display = "PipeError")]
+    PipeError(#[fail(cause)] PipeError),
+    #[fail(display = "HResultMessage")]
+    HResultMessage(#[fail(cause)] HResultMessage),
+}
+
+impl convert::From<PipeError> for MyError {
+    fn from(err: PipeError) -> MyError {
+        MyError::PipeError(err)
+    }
+}
+
+impl convert::From<comedy::HResult> for MyError {
+    fn from(err: comedy::HResult) -> MyError {
+        MyError::HResult(err)
+    }
+}
+
+impl convert::From<comedy::Win32Error> for MyError {
+    fn from(err: comedy::Win32Error) -> MyError {
+        MyError::Win32Error(err)
+    }
+}
+
+impl convert::From<HResultMessage> for MyError {
+    fn from(err: HResultMessage) -> MyError {
+        MyError::HResultMessage(err)
+    }
+}
+
+macro_rules! bail {
+    ($e:expr) => {
+        return Err($crate::MyError::Msg($e.to_string()));
+    };
+    ($fmt:expr, $($arg:tt)*) => {
+        return Err($crate::MyError::Msg(format!($fmt, $($arg)*)));
+    };
+}
+
+type Result = std::result::Result<(), MyError>;
+
+pub fn main() {
+    if let Err(err) = entry() {
+        eprintln!("{}", err);
+        for cause in err.as_fail().iter_causes() {
+            eprintln!("caused by {}", cause);
+        }
+
+        process::exit(1);
+    } else {
+        println!("OK");
+    }
+}
+
+const EXE_NAME: &'static str = "test_client";
+
+fn usage() -> String {
+    format!(
+        concat!(
+            "Usage {0} <command> ",
+            "[local-service] ",
+            "[args...]\n",
+            "Commands:\n",
+            "  bits-start <URL> <local file>\n",
+            "  bits-monitor <GUID>\n",
+            "  bits-bg <GUID>\n",
+            "  bits-fg <GUID>\n",
+            "  bits-suspend <GUID>\n",
+            "  bits-resume <GUID>\n",
+            "  bits-complete <GUID>\n",
+            "  bits-cancel <GUID> ...\n"
+        ),
+        EXE_NAME
+    )
+}
+
+fn entry() -> Result {
+    let args: Vec<_> = env::args_os().collect();
+
+    let mut client = BitsClient::new(
+        OsString::from("bits_client test"),
+        OsString::from("C:\\ProgramData"),
+    )?;
+
+    if args.len() < 2 {
+        eprintln!("{}", usage());
+        bail!("not enough arguments");
+    }
+
+    let cmd = &*args[1].to_string_lossy();
+    let cmd_args = &args[2..];
+
+    match cmd {
+        // command line client for testing
+        "bits-start" if cmd_args.len() == 2 => bits_start(
+            Arc::new(Mutex::new(client)),
+            cmd_args[0].clone(),
+            cmd_args[1].clone(),
+            BitsProxyUsage::Preconfig,
+        ),
+        "bits-monitor" if cmd_args.len() == 1 => {
+            bits_monitor(Arc::new(Mutex::new(client)), &cmd_args[0])
+        }
+        // TODO: some way of testing set update interval
+        "bits-bg" if cmd_args.len() == 1 => bits_bg(&mut client, &cmd_args[0]),
+        "bits-fg" if cmd_args.len() == 1 => bits_fg(&mut client, &cmd_args[0]),
+        "bits-suspend" if cmd_args.len() == 1 => bits_suspend(&mut client, &cmd_args[0]),
+        "bits-resume" if cmd_args.len() == 1 => bits_resume(&mut client, &cmd_args[0]),
+        "bits-complete" if cmd_args.len() == 1 => bits_complete(&mut client, &cmd_args[0]),
+        "bits-cancel" if cmd_args.len() >= 1 => {
+            for guid in cmd_args {
+                bits_cancel(&mut client, guid)?;
+            }
+            Ok(())
+        }
+        _ => {
+            eprintln!("{}", usage());
+            bail!("usage error");
+        }
+    }
+}
+
+fn bits_start(
+    client: Arc<Mutex<BitsClient>>,
+    url: OsString,
+    save_path: OsString,
+    proxy_usage: BitsProxyUsage,
+) -> Result {
+    //let interval = 10 * 60 * 1000;
+    let interval = 1000;
+
+    let result = client
+        .lock()
+        .unwrap()
+        .start_job(url, save_path, proxy_usage, interval)?;
+
+    match result {
+        Ok((r, monitor_client)) => {
+            println!("start success, guid = {}", r.guid);
+            client
+                .lock()
+                .unwrap()
+                .set_update_interval(r.guid.clone(), interval)?
+                .unwrap();
+            monitor_loop(client, monitor_client, r.guid.clone(), interval)?;
+            Ok(())
+        }
+        Err(e) => {
+            let _ = e.clone();
+            bail!("error from server {}", e)
+        }
+    }
+}
+
+fn bits_monitor(client: Arc<Mutex<BitsClient>>, guid: &OsStr) -> Result {
+    let guid = Guid::from_str(&guid.to_string_lossy())?;
+    let result = client.lock().unwrap().monitor_job(guid.clone(), 1000)?;
+    match result {
+        Ok(monitor_client) => {
+            println!("monitor success");
+            monitor_loop(client, monitor_client, guid, 1000)?;
+            Ok(())
+        }
+        Err(e) => bail!("error from server {}", e),
+    }
+}
+
+fn _check_client_send()
+where
+    BitsClient: Send,
+{
+}
+fn _check_monitor_send()
+where
+    BitsMonitorClient: Send,
+{
+}
+
+fn monitor_loop(
+    _client: Arc<Mutex<BitsClient>>,
+    mut monitor_client: BitsMonitorClient,
+    _guid: Guid,
+    wait_millis: u32,
+) -> Result {
+    /*
+    // Commented out to avoid vendoring ctrlc.
+    // This could also possibly be done with `exclude` in the mozilla-central `Cargo.toml`.
+    let client_for_handler = _client.clone();
+    ctrlc::set_handler(move || {
+        eprintln!("Ctrl-C!");
+        let _ = client_for_handler.lock().unwrap().stop_update(_guid.clone());
+    })
+    .expect("Error setting Ctrl-C handler");
+    */
+
+    loop {
+        let status = monitor_client.get_status(wait_millis * 10)??;
+
+        println!("{:?} {:?}", BitsJobState::from(status.state), status);
+
+        //println!("{}", job.get_first_file()?.get_remote_name()?.into_string().unwrap());
+        let transfer_completion_time = if let Some(ft) = status.times.transfer_completion {
+            format!("Some({})", ft.to_system_time_utc()?)
+        } else {
+            String::from("None")
+        };
+        println!(
+            "creation: {}, modification: {}, transfer completion: {}",
+            status.times.creation.to_system_time_utc()?,
+            status.times.modification.to_system_time_utc()?,
+            transfer_completion_time
+        );
+
+        match BitsJobState::from(status.state) {
+            BitsJobState::Connecting
+            | BitsJobState::Transferring
+            | BitsJobState::TransientError => {}
+            _ => break,
+        }
+    }
+    println!("monitor loop ending");
+    println!("sleeping...");
+    std::thread::sleep(std::time::Duration::from_secs(1));
+
+    Ok(())
+}
+
+fn bits_bg(client: &mut BitsClient, guid: &OsStr) -> Result {
+    bits_set_priority(client, guid, false)
+}
+
+fn bits_fg(client: &mut BitsClient, guid: &OsStr) -> Result {
+    bits_set_priority(client, guid, true)
+}
+
+fn bits_set_priority(client: &mut BitsClient, guid: &OsStr, foreground: bool) -> Result {
+    let guid = Guid::from_str(&guid.to_string_lossy())?;
+    match client.set_job_priority(guid, foreground)? {
+        Ok(()) => Ok(()),
+        Err(e) => bail!("error from server {}", e),
+    }
+}
+
+fn bits_suspend(client: &mut BitsClient, guid: &OsStr) -> Result {
+    let guid = Guid::from_str(&guid.to_string_lossy())?;
+    match client.suspend_job(guid)? {
+        Ok(()) => Ok(()),
+        Err(e) => bail!("error from server {}", e),
+    }
+}
+
+fn bits_resume(client: &mut BitsClient, guid: &OsStr) -> Result {
+    let guid = Guid::from_str(&guid.to_string_lossy())?;
+    match client.resume_job(guid)? {
+        Ok(()) => Ok(()),
+        Err(e) => bail!("error from server {}", e),
+    }
+}
+
+fn bits_complete(client: &mut BitsClient, guid: &OsStr) -> Result {
+    let guid = Guid::from_str(&guid.to_string_lossy())?;
+    match client.complete_job(guid)? {
+        Ok(()) => Ok(()),
+        Err(e) => bail!("error from server {}", e),
+    }
+}
+
+fn bits_cancel(client: &mut BitsClient, guid: &OsStr) -> Result {
+    let guid = Guid::from_str(&guid.to_string_lossy())?;
+    match client.cancel_job(guid)? {
+        Ok(()) => Ok(()),
+        Err(e) => bail!("error from server {}", e),
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/bits_protocol.rs
@@ -0,0 +1,329 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Command, response, and status types.
+
+use std::ffi::OsString;
+use std::fmt;
+use std::result;
+
+use failure::Fail;
+use guid_win::Guid;
+
+use super::{BitsErrorContext, BitsJobProgress, BitsJobState, BitsJobTimes, BitsProxyUsage};
+
+type HRESULT = i32;
+
+/// An HRESULT with a descriptive message
+#[derive(Clone, Debug, Fail)]
+pub struct HResultMessage {
+    pub hr: HRESULT,
+    pub message: String,
+}
+
+impl fmt::Display for HResultMessage {
+    fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
+        self.message.fmt(f)
+    }
+}
+
+/// Commands which can be sent to the server.
+///
+/// This is currently unused as the out-of-process Local Service server is not finished.
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub enum Command {
+    StartJob(StartJobCommand),
+    MonitorJob(MonitorJobCommand),
+    SuspendJob(SuspendJobCommand),
+    ResumeJob(ResumeJobCommand),
+    SetJobPriority(SetJobPriorityCommand),
+    SetUpdateInterval(SetUpdateIntervalCommand),
+    CompleteJob(CompleteJobCommand),
+    CancelJob(CancelJobCommand),
+}
+
+/// Combine a [`Command`](enum.Command.html) with its success and failure result types.
+#[doc(hidden)]
+pub trait CommandType {
+    type Success;
+    type Failure: Fail;
+    fn wrap(command: Self) -> Command;
+}
+
+// Start Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct StartJobCommand {
+    pub url: OsString,
+    pub save_path: OsString,
+    pub proxy_usage: BitsProxyUsage,
+    pub monitor: Option<MonitorConfig>,
+}
+
+impl CommandType for StartJobCommand {
+    type Success = StartJobSuccess;
+    type Failure = StartJobFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::StartJob(cmd)
+    }
+}
+
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct MonitorConfig {
+    pub pipe_name: OsString,
+    pub interval_millis: u32,
+}
+
+#[derive(Clone, Debug)]
+pub struct StartJobSuccess {
+    pub guid: Guid,
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum StartJobFailure {
+    #[fail(display = "Argument validation failed: {}", _0)]
+    ArgumentValidation(String),
+    #[fail(display = "Create job: {}", _0)]
+    Create(HResultMessage),
+    #[fail(display = "Add file to job: {}", _0)]
+    AddFile(HResultMessage),
+    #[fail(display = "Apply settings to job: {}", _0)]
+    ApplySettings(HResultMessage),
+    #[fail(display = "Resume job: {}", _0)]
+    Resume(HResultMessage),
+    #[fail(display = "BITS error: {}", _0)]
+    OtherBITS(HResultMessage),
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+// Monitor Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct MonitorJobCommand {
+    pub guid: Guid,
+    pub monitor: MonitorConfig,
+}
+
+impl CommandType for MonitorJobCommand {
+    type Success = ();
+    type Failure = MonitorJobFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::MonitorJob(cmd)
+    }
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum MonitorJobFailure {
+    #[fail(display = "Argument validation failed: {}", _0)]
+    ArgumentValidation(String),
+    #[fail(display = "Job not found")]
+    NotFound,
+    #[fail(display = "Get job: {}", _0)]
+    GetJob(HResultMessage),
+    #[fail(display = "BITS error: {}", _0)]
+    OtherBITS(HResultMessage),
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+// Suspend Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct SuspendJobCommand {
+    pub guid: Guid,
+}
+
+impl CommandType for SuspendJobCommand {
+    type Success = ();
+    type Failure = SuspendJobFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::SuspendJob(cmd)
+    }
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum SuspendJobFailure {
+    #[fail(display = "Job not found")]
+    NotFound,
+    #[fail(display = "Get job: {}", _0)]
+    GetJob(HResultMessage),
+    #[fail(display = "Suspend job: {}", _0)]
+    SuspendJob(HResultMessage),
+    #[fail(display = "BITS error: {}", _0)]
+    OtherBITS(HResultMessage),
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+// Resume Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct ResumeJobCommand {
+    pub guid: Guid,
+}
+
+impl CommandType for ResumeJobCommand {
+    type Success = ();
+    type Failure = ResumeJobFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::ResumeJob(cmd)
+    }
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum ResumeJobFailure {
+    #[fail(display = "Job not found")]
+    NotFound,
+    #[fail(display = "Get job: {}", _0)]
+    GetJob(HResultMessage),
+    #[fail(display = "Resume job: {}", _0)]
+    ResumeJob(HResultMessage),
+    #[fail(display = "BITS error: {}", _0)]
+    OtherBITS(HResultMessage),
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+// Set Job Priority
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct SetJobPriorityCommand {
+    pub guid: Guid,
+    pub foreground: bool,
+}
+
+impl CommandType for SetJobPriorityCommand {
+    type Success = ();
+    type Failure = SetJobPriorityFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::SetJobPriority(cmd)
+    }
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum SetJobPriorityFailure {
+    #[fail(display = "Job not found")]
+    NotFound,
+    #[fail(display = "Get job: {}", _0)]
+    GetJob(HResultMessage),
+    #[fail(display = "Apply settings to job: {}", _0)]
+    ApplySettings(HResultMessage),
+    #[fail(display = "BITS error: {}", _0)]
+    OtherBITS(HResultMessage),
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+// Set Update Interval
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct SetUpdateIntervalCommand {
+    pub guid: Guid,
+    pub interval_millis: u32,
+}
+
+impl CommandType for SetUpdateIntervalCommand {
+    type Success = ();
+    type Failure = SetUpdateIntervalFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::SetUpdateInterval(cmd)
+    }
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum SetUpdateIntervalFailure {
+    #[fail(display = "Argument validation: {}", _0)]
+    ArgumentValidation(String),
+    #[fail(display = "Monitor not found")]
+    NotFound,
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+// Complete Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct CompleteJobCommand {
+    pub guid: Guid,
+}
+
+impl CommandType for CompleteJobCommand {
+    type Success = ();
+    type Failure = CompleteJobFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::CompleteJob(cmd)
+    }
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum CompleteJobFailure {
+    #[fail(display = "Job not found")]
+    NotFound,
+    #[fail(display = "Get job: {}", _0)]
+    GetJob(HResultMessage),
+    #[fail(display = "Complete job: {}", _0)]
+    CompleteJob(HResultMessage),
+    #[fail(display = "Job only partially completed")]
+    PartialComplete,
+    #[fail(display = "BITS error: {}", _0)]
+    OtherBITS(HResultMessage),
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+// Cancel Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct CancelJobCommand {
+    pub guid: Guid,
+}
+
+impl CommandType for CancelJobCommand {
+    type Success = ();
+    type Failure = CancelJobFailure;
+    fn wrap(cmd: Self) -> Command {
+        Command::CancelJob(cmd)
+    }
+}
+
+#[derive(Clone, Debug, Fail)]
+pub enum CancelJobFailure {
+    #[fail(display = "Job not found")]
+    NotFound,
+    #[fail(display = "Get job: {}", _0)]
+    GetJob(HResultMessage),
+    #[fail(display = "Cancel job: {}", _0)]
+    CancelJob(HResultMessage),
+    #[fail(display = "BITS error: {}", _0)]
+    OtherBITS(HResultMessage),
+    #[fail(display = "Other failure: {}", _0)]
+    Other(String),
+}
+
+/// Job status report
+///
+/// This includes a URL which updates with redirect but is otherwise the same as
+/// `bits::status::BitsJobStatus`.
+#[derive(Clone, Debug)]
+pub struct JobStatus {
+    pub state: BitsJobState,
+    pub progress: BitsJobProgress,
+    pub error_count: u32,
+    pub error: Option<JobError>,
+    pub times: BitsJobTimes,
+    /// None means same as last time
+    pub url: Option<OsString>,
+}
+
+/// Job error report
+#[derive(Clone, Debug, Fail)]
+#[fail(display = "Job error in context {}: {}", context_str, error)]
+pub struct JobError {
+    pub context: BitsErrorContext,
+    pub context_str: String,
+    pub error: HResultMessage,
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/in_process/mod.rs
@@ -0,0 +1,473 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cmp;
+use std::collections::{hash_map, HashMap};
+use std::ffi;
+use std::path;
+use std::sync::{Arc, Condvar, Mutex, Weak};
+use std::time::{Duration, Instant};
+
+use bits::{
+    BackgroundCopyManager, BitsJob, BitsJobPriority, BitsProxyUsage, BG_S_PARTIAL_COMPLETE, E_FAIL,
+};
+use guid_win::Guid;
+
+use bits_protocol::*;
+
+use super::Error;
+
+// This is a macro in order to use the NotFound and GetJob variants from whatever enum is in scope.
+macro_rules! get_job {
+    ($bcm:ident, $guid:expr, $name:expr) => {{
+        $bcm = BackgroundCopyManager::connect().map_err(|e| Other(e.to_string()))?;
+        $bcm.find_job_by_guid_and_name($guid, $name)
+            .map_err(|e| GetJob($crate::in_process::format_error(&$bcm, e)))?
+            .ok_or(NotFound)?
+    }};
+}
+
+fn format_error(bcm: &BackgroundCopyManager, error: comedy::HResult) -> HResultMessage {
+    let bits_description = bcm.get_error_description(error.code()).ok();
+
+    HResultMessage {
+        hr: error.code(),
+        message: if let Some(desc) = bits_description {
+            format!("{}: {}", error, desc)
+        } else {
+            format!("{}", error)
+        },
+    }
+}
+
+// The in-process client uses direct BITS calls via the `bits` crate.
+// See the corresponding functions in BitsClient.
+pub struct InProcessClient {
+    job_name: ffi::OsString,
+    save_path_prefix: path::PathBuf,
+    monitors: HashMap<Guid, InProcessMonitorControl>,
+}
+
+impl InProcessClient {
+    pub fn new(
+        job_name: ffi::OsString,
+        save_path_prefix: ffi::OsString,
+    ) -> Result<InProcessClient, Error> {
+        Ok(InProcessClient {
+            job_name,
+            save_path_prefix: path::PathBuf::from(save_path_prefix),
+            monitors: HashMap::new(),
+        })
+    }
+
+    pub fn start_job(
+        &mut self,
+        url: ffi::OsString,
+        save_path: ffi::OsString,
+        proxy_usage: BitsProxyUsage,
+        monitor_interval_millis: u32,
+    ) -> Result<(StartJobSuccess, InProcessMonitor), StartJobFailure> {
+        use StartJobFailure::*;
+
+        let full_path = self.save_path_prefix.join(save_path);
+
+        // Verify that `full_path` is under the directory called `save_path_prefix`.
+        {
+            let canonical_prefix = self.save_path_prefix.canonicalize().map_err(|e| {
+                ArgumentValidation(format!("save_path_prefix.canonicalize(): {}", e))
+            })?;
+            // Full path minus file name, canonicalize() fails with nonexistent files, but the
+            // parent directory ought to exist.
+            let canonical_full_path = full_path
+                .parent()
+                .ok_or_else(|| ArgumentValidation("full_path.parent(): None".into()))?
+                .canonicalize()
+                .map_err(|e| {
+                    ArgumentValidation(format!("full_path.parent().canonicalize(): {}", e))
+                })?;
+
+            if !canonical_full_path.starts_with(&canonical_prefix) {
+                return Err(ArgumentValidation(format!(
+                    "{:?} is not within {:?}",
+                    canonical_full_path, canonical_prefix
+                )));
+            }
+        }
+
+        // TODO: Should the job be explicitly cleaned up if this fn can't return success?
+        // If the job is dropped before `AddFile` succeeds, I think it automatically gets
+        // deleted from the queue. There is only one fallible call after that (`Resume`).
+
+        let bcm = BackgroundCopyManager::connect().map_err(|e| Other(e.to_string()))?;
+        let mut job = bcm
+            .create_job(&self.job_name)
+            .map_err(|e| Create(format_error(&bcm, e)))?;
+
+        let guid = job.guid().map_err(|e| OtherBITS(format_error(&bcm, e)))?;
+
+        (|| {
+            job.set_proxy_usage(proxy_usage)?;
+            job.set_minimum_retry_delay(60)?;
+            job.set_redirect_report()?;
+
+            job.set_priority(BitsJobPriority::Foreground)?;
+
+            Ok(())
+        })()
+        .map_err(|e| ApplySettings(format_error(&bcm, e)))?;
+
+        let (client, control) = InProcessMonitor::new(&mut job, monitor_interval_millis)
+            .map_err(|e| OtherBITS(format_error(&bcm, e)))?;
+
+        job.add_file(&url, &full_path.into_os_string())
+            .map_err(|e| AddFile(format_error(&bcm, e)))?;
+
+        job.resume().map_err(|e| Resume(format_error(&bcm, e)))?;
+
+        self.monitors.insert(guid.clone(), control);
+
+        Ok((StartJobSuccess { guid }, client))
+    }
+
+    pub fn monitor_job(
+        &mut self,
+        guid: Guid,
+        interval_millis: u32,
+    ) -> Result<InProcessMonitor, MonitorJobFailure> {
+        use MonitorJobFailure::*;
+
+        // Stop any preexisting monitor for the same guid.
+        let _ = self.stop_update(guid.clone());
+
+        let bcm;
+        let (client, control) =
+            InProcessMonitor::new(&mut get_job!(bcm, &guid, &self.job_name), interval_millis)
+                .map_err(|e| OtherBITS(format_error(&bcm, e)))?;
+
+        self.monitors.insert(guid, control);
+
+        Ok(client)
+    }
+
+    pub fn suspend_job(&mut self, guid: Guid) -> Result<(), SuspendJobFailure> {
+        use SuspendJobFailure::*;
+
+        let bcm;
+        get_job!(bcm, &guid, &self.job_name)
+            .suspend()
+            .map_err(|e| SuspendJob(format_error(&bcm, e)))?;
+
+        Ok(())
+    }
+
+    pub fn resume_job(&mut self, guid: Guid) -> Result<(), ResumeJobFailure> {
+        use ResumeJobFailure::*;
+
+        let bcm;
+        get_job!(bcm, &guid, &self.job_name)
+            .resume()
+            .map_err(|e| ResumeJob(format_error(&bcm, e)))?;
+
+        Ok(())
+    }
+
+    pub fn set_job_priority(
+        &mut self,
+        guid: Guid,
+        foreground: bool,
+    ) -> Result<(), SetJobPriorityFailure> {
+        use SetJobPriorityFailure::*;
+
+        let priority = if foreground {
+            BitsJobPriority::Foreground
+        } else {
+            BitsJobPriority::Normal
+        };
+
+        let bcm;
+        get_job!(bcm, &guid, &self.job_name)
+            .set_priority(priority)
+            .map_err(|e| ApplySettings(format_error(&bcm, e)))?;
+
+        Ok(())
+    }
+
+    fn get_monitor_control_sender(&mut self, guid: Guid) -> Option<Arc<ControlPair>> {
+        if let hash_map::Entry::Occupied(occ) = self.monitors.entry(guid) {
+            if let Some(sender) = occ.get().0.upgrade() {
+                Some(sender)
+            } else {
+                // Remove dangling Weak
+                occ.remove_entry();
+                None
+            }
+        } else {
+            None
+        }
+    }
+
+    pub fn set_update_interval(
+        &mut self,
+        guid: Guid,
+        interval_millis: u32,
+    ) -> Result<(), SetUpdateIntervalFailure> {
+        use SetUpdateIntervalFailure::*;
+
+        if let Some(sender) = self.get_monitor_control_sender(guid) {
+            let mut s = sender.1.lock().unwrap();
+            s.interval_millis = interval_millis;
+            sender.0.notify_all();
+            Ok(())
+        } else {
+            Err(NotFound)
+        }
+    }
+
+    pub fn stop_update(&mut self, guid: Guid) -> Result<(), SetUpdateIntervalFailure> {
+        use SetUpdateIntervalFailure::*;
+
+        if let Some(sender) = self.get_monitor_control_sender(guid) {
+            sender.1.lock().unwrap().shutdown = true;
+            sender.0.notify_all();
+            Ok(())
+        } else {
+            Err(NotFound)
+        }
+    }
+
+    pub fn complete_job(&mut self, guid: Guid) -> Result<(), CompleteJobFailure> {
+        use CompleteJobFailure::*;
+
+        let bcm;
+        get_job!(bcm, &guid, &self.job_name)
+            .complete()
+            .map_err(|e| CompleteJob(format_error(&bcm, e)))
+            .and_then(|hr| {
+                if hr == BG_S_PARTIAL_COMPLETE as i32 {
+                    Err(PartialComplete)
+                } else {
+                    Ok(())
+                }
+            })?;
+
+        let _ = self.stop_update(guid);
+
+        Ok(())
+    }
+
+    pub fn cancel_job(&mut self, guid: Guid) -> Result<(), CancelJobFailure> {
+        use CancelJobFailure::*;
+
+        let bcm;
+        get_job!(bcm, &guid, &self.job_name)
+            .cancel()
+            .map_err(|e| CancelJob(format_error(&bcm, e)))?;
+
+        let _ = self.stop_update(guid);
+
+        Ok(())
+    }
+}
+
+// InProcessMonitor can be used on any thread, and `ControlPair` can be synchronously modified to
+// control a blocked `get_status` call from another thread.
+pub struct InProcessMonitor {
+    vars: Arc<ControlPair>,
+    guid: Guid,
+    last_status_time: Option<Instant>,
+    last_url: Option<ffi::OsString>,
+}
+
+// The `Condvar` is notified when `InProcessMonitorVars` changes.
+type ControlPair = (Condvar, Mutex<InProcessMonitorVars>);
+struct InProcessMonitorControl(Weak<ControlPair>);
+
+// RefUnwindSafe is not impl'd for Condvar but likely should be,
+// see https://github.com/rust-lang/rust/issues/54768
+impl std::panic::RefUnwindSafe for InProcessMonitorControl {}
+
+struct InProcessMonitorVars {
+    interval_millis: u32,
+    notified: bool,
+    shutdown: bool,
+}
+
+impl InProcessMonitor {
+    fn new(
+        job: &mut BitsJob,
+        interval_millis: u32,
+    ) -> Result<(InProcessMonitor, InProcessMonitorControl), comedy::HResult> {
+        let guid = job.guid()?;
+
+        let vars = Arc::new((
+            Condvar::new(),
+            Mutex::new(InProcessMonitorVars {
+                interval_millis,
+                notified: false,
+                shutdown: false,
+            }),
+        ));
+
+        let transferred_control = InProcessMonitorControl(Arc::downgrade(&vars));
+        let transferred_cb = Box::new(move || {
+            if let Some(control) = transferred_control.0.upgrade() {
+                if let Ok(mut vars) = control.1.lock() {
+                    vars.notified = true;
+                    control.0.notify_all();
+                    return Ok(());
+                }
+            }
+            Err(E_FAIL)
+        });
+
+        let error_control = InProcessMonitorControl(Arc::downgrade(&vars));
+        let error_cb = Box::new(move || {
+            if let Some(control) = error_control.0.upgrade() {
+                if let Ok(mut vars) = control.1.lock() {
+                    vars.notified = true;
+                    control.0.notify_all();
+                    return Ok(());
+                }
+            }
+            Err(E_FAIL)
+        });
+
+        // Note: These callbacks are never explicitly cleared. They will be freed when the
+        // job is deleted from BITS, and they will be cleared if an attempt is made to call them
+        // when they are no longer valid (e.g. after the process exits). This is done mostly for
+        // simplicity and should be safe.
+
+        job.register_callbacks(Some(transferred_cb), Some(error_cb), None)?;
+
+        let control = InProcessMonitorControl(Arc::downgrade(&vars));
+
+        let monitor = InProcessMonitor {
+            guid,
+            vars,
+            last_status_time: None,
+            last_url: None,
+        };
+
+        Ok((monitor, control))
+    }
+
+    pub fn get_status(
+        &mut self,
+        timeout_millis: u32,
+    ) -> Result<Result<JobStatus, HResultMessage>, Error> {
+        let timeout = Duration::from_millis(u64::from(timeout_millis));
+
+        let started = Instant::now();
+
+        {
+            let mut s = self.vars.1.lock().unwrap();
+            loop {
+                if s.shutdown {
+                    // Disconnected, immediately return error.
+                    // Note: Shutdown takes priority over simultaneous notification.
+                    return Err(Error::NotConnected);
+                }
+
+                if started.elapsed() > timeout {
+                    // Timed out, immediately return timeout error.
+                    // This should not normally happen with the in-process monitor, but e.g. the
+                    // monitor interval could be longer than the timeout.
+                    s.shutdown = true;
+                    return Err(Error::Timeout);
+                }
+
+                // Get the interval every pass through the loop, in case it has changed.
+                let interval = Duration::from_millis(u64::from(s.interval_millis));
+
+                let wait_until = self.last_status_time.map(|last_status_time| {
+                    cmp::min(last_status_time + interval, started + timeout)
+                });
+
+                if s.notified {
+                    // Notified, exit loop to get status.
+                    s.notified = false;
+                    break;
+                }
+
+                if wait_until.is_none() {
+                    // First status report, no waiting, exit loop to get status.
+                    break;
+                }
+
+                let wait_until = wait_until.unwrap();
+                let wait_start = Instant::now();
+
+                if wait_until <= wait_start {
+                    // Status report due, exit loop to get status.
+                    break;
+                }
+
+                // Wait.
+                // Do not attempt to recover from poisoned Mutex.
+                s = self
+                    .vars
+                    .0
+                    .wait_timeout(s, wait_until - wait_start)
+                    .unwrap()
+                    .0;
+
+                // Mutex re-acquired, loop.
+            }
+        }
+
+        // No error yet, start getting status now.
+        self.last_status_time = Some(Instant::now());
+
+        let bcm = match BackgroundCopyManager::connect() {
+            Ok(bcm) => bcm,
+            Err(e) => {
+                // On any error, disconnect.
+                self.vars.1.lock().unwrap().shutdown = true;
+
+                // Errors below can use the BCM to do `format_error()`, but this one just gets the
+                // basic `comedy::HResult` treatment.
+                return Ok(Err(HResultMessage {
+                    hr: e.code(),
+                    message: format!("{}", e),
+                }));
+            }
+        };
+
+        Ok((|| {
+            let mut job = bcm.get_job_by_guid(&self.guid)?;
+
+            let status = job.get_status()?;
+            let url = job.get_first_file()?.get_remote_name()?;
+
+            Ok(JobStatus {
+                state: status.state,
+                progress: status.progress,
+                error_count: status.error_count,
+                error: status.error.map(|e| JobError {
+                    context: e.context,
+                    context_str: e.context_str,
+                    error: HResultMessage {
+                        hr: e.error,
+                        message: e.error_str,
+                    },
+                }),
+                times: status.times,
+                url: if self.last_url.is_some() && *self.last_url.as_ref().unwrap() == url {
+                    None
+                } else {
+                    self.last_url = Some(url);
+                    self.last_url.clone()
+                },
+            })
+        })()
+        .map_err(|e| {
+            // On any error, disconnect.
+            self.vars.1.lock().unwrap().shutdown = true;
+            format_error(&bcm, e)
+        }))
+    }
+}
+
+#[cfg(test)]
+mod tests;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/in_process/tests.rs
@@ -0,0 +1,518 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+// These are full integration tests that use the BITS service.
+
+// TODO
+// It may make sense to restrict how many tests can run at once. BITS is only supposed to support
+// four simultaneous notifications per user, it is not impossible that this test suite could
+// exceed that.
+
+#![cfg(test)]
+extern crate bits;
+extern crate lazy_static;
+extern crate rand;
+extern crate regex;
+extern crate tempdir;
+
+use std::ffi::{OsStr, OsString};
+use std::fs::{self, File};
+use std::io::{Read, Write};
+use std::net::{SocketAddr, TcpListener, TcpStream};
+use std::panic;
+use std::sync::{Arc, Condvar, Mutex};
+use std::thread::{self, JoinHandle};
+use std::time::{Duration, Instant};
+
+use self::{
+    bits::BackgroundCopyManager,
+    lazy_static::lazy_static,
+    rand::{thread_rng, Rng},
+    regex::bytes::Regex,
+    tempdir::TempDir,
+};
+use super::{
+    super::{BitsJobState, Error},
+    BitsProxyUsage, InProcessClient, StartJobSuccess,
+};
+
+static SERVER_ADDRESS: [u8; 4] = [127, 0, 0, 1];
+
+lazy_static! {
+    static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
+}
+
+fn format_job_name(name: &str) -> OsString {
+    format!("InProcessClient Test {}", name).into()
+}
+
+fn format_dir_prefix(tmp_dir: &TempDir) -> OsString {
+    let mut dir = tmp_dir.path().to_path_buf().into_os_string();
+    dir.push("\\");
+    dir
+}
+
+fn cancel_jobs(name: &OsStr) {
+    BackgroundCopyManager::connect()
+        .unwrap()
+        .cancel_jobs_by_name(name)
+        .unwrap();
+}
+
+struct HttpServerResponses {
+    body: Box<[u8]>,
+    delay: u64,
+    //error: Box<[u8]>,
+}
+
+struct MockHttpServerHandle {
+    port: u16,
+    join: Option<JoinHandle<Result<(), ()>>>,
+    shutdown: Arc<(Mutex<bool>, Condvar)>,
+}
+
+impl MockHttpServerHandle {
+    fn shutdown(&mut self) {
+        if self.join.is_none() {
+            return;
+        }
+
+        {
+            let &(ref lock, ref cvar) = &*self.shutdown;
+            let mut shutdown = lock.lock().unwrap();
+
+            if !*shutdown {
+                *shutdown = true;
+                cvar.notify_all();
+            }
+        }
+        // Wake up the server from `accept()`. Will fail if the server wasn't listening.
+        let _ = TcpStream::connect_timeout(
+            &(SERVER_ADDRESS, self.port).into(),
+            Duration::from_millis(10_000),
+        );
+
+        match self.join.take().unwrap().join() {
+            Ok(_) => {}
+            Err(p) => panic::resume_unwind(p),
+        }
+    }
+
+    fn format_url(&self, name: &str) -> OsString {
+        format!(
+            "http://{}/{}",
+            SocketAddr::from((SERVER_ADDRESS, self.port)),
+            name
+        )
+        .into()
+    }
+}
+
+fn mock_http_server(name: &'static str, responses: HttpServerResponses) -> MockHttpServerHandle {
+    let mut bind_retries = 10;
+    let shutdown = Arc::new((Mutex::new(false), Condvar::new()));
+    let caller_shutdown = shutdown.clone();
+
+    let (listener, port) = loop {
+        let port = thread_rng().gen_range(1024, 0x1_0000u32) as u16;
+        match TcpListener::bind(SocketAddr::from((SERVER_ADDRESS, port))) {
+            Ok(listener) => {
+                break (listener, port);
+            }
+            r @ Err(_) => {
+                if bind_retries == 0 {
+                    r.unwrap();
+                }
+                bind_retries -= 1;
+                continue;
+            }
+        }
+    };
+
+    let join = thread::Builder::new()
+        .name(format!("mock_http_server {}", name))
+        .spawn(move || {
+            // returns Err(()) if server should shut down immediately
+            fn check_shutdown(shutdown: &Arc<(Mutex<bool>, Condvar)>) -> Result<(), ()> {
+                if *shutdown.0.lock().unwrap() {
+                    Err(())
+                } else {
+                    Ok(())
+                }
+            }
+            fn sleep(shutdown: &Arc<(Mutex<bool>, Condvar)>, delay_millis: u64) -> Result<(), ()> {
+                let sleep_start = Instant::now();
+                let sleep_end = sleep_start + Duration::from_millis(delay_millis);
+
+                let (ref lock, ref cvar) = **shutdown;
+                let mut shutdown_requested = lock.lock().unwrap();
+                loop {
+                    if *shutdown_requested {
+                        return Err(());
+                    }
+
+                    let before_wait = Instant::now();
+                    if before_wait >= sleep_end {
+                        return Ok(());
+                    }
+                    let wait_dur = sleep_end - before_wait;
+                    shutdown_requested = cvar.wait_timeout(shutdown_requested, wait_dur).unwrap().0;
+                }
+            }
+
+            let error_404 = Regex::new(r"^((GET)|(HEAD)) [[:print:]]*/error_404 ").unwrap();
+            let error_500 = Regex::new(r"^((GET)|(HEAD)) [[:print:]]*/error_500 ").unwrap();
+
+            loop {
+                let (mut socket, _addr) = listener.accept().expect("accept should succeed");
+
+                socket
+                    .set_read_timeout(Some(Duration::from_millis(10_000)))
+                    .unwrap();
+                let mut s = Vec::new();
+                for b in Read::by_ref(&mut socket).bytes() {
+                    if b.is_err() {
+                        eprintln!("read error {:?}", b);
+                        break;
+                    }
+                    let b = b.unwrap();
+                    s.push(b);
+                    if s.ends_with(b"\r\n\r\n") {
+                        break;
+                    }
+                    check_shutdown(&shutdown)?;
+                }
+
+                // request received
+
+                check_shutdown(&shutdown)?;
+
+                // Special error URIs
+                if error_404.is_match(&s) {
+                    sleep(&shutdown, responses.delay)?;
+                    let result = socket
+                        .write(b"HTTP/1.1 404 Not Found\r\n\r\n")
+                        .and_then(|_| socket.flush());
+                    if let Err(e) = result {
+                        eprintln!("error writing 404 header {:?}", e);
+                    }
+                    continue;
+                }
+
+                if error_500.is_match(&s) {
+                    sleep(&shutdown, responses.delay)?;
+                    let result = socket
+                        .write(b"HTTP/1.1 500 Internal Server Error\r\n\r\n")
+                        .and_then(|_| socket.flush());
+                    if let Err(e) = result {
+                        eprintln!("error writing 500 header {:?}", e);
+                    }
+                    continue;
+                }
+
+                // Response with a body.
+                if s.starts_with(b"HEAD") || s.starts_with(b"GET") {
+                    let result = socket
+                        .write(
+                            format!(
+                                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n",
+                                responses.body.len()
+                            )
+                            .as_bytes(),
+                        )
+                        .and_then(|_| socket.flush());
+                    if let Err(e) = result {
+                        eprintln!("error writing header {:?}", e);
+                        continue;
+                    }
+                }
+
+                if s.starts_with(b"GET") {
+                    sleep(&shutdown, responses.delay)?;
+                    let result = socket.write(&responses.body).and_then(|_| socket.flush());
+                    if let Err(e) = result {
+                        eprintln!("error writing content {:?}", e);
+                        continue;
+                    }
+                }
+            }
+        });
+
+    MockHttpServerHandle {
+        port,
+        join: Some(join.unwrap()),
+        shutdown: caller_shutdown,
+    }
+}
+
+// Test wrapper to ensure jobs are canceled, set up name strings
+macro_rules! test {
+    (fn $name:ident($param:ident : &str, $tmpdir:ident : &TempDir) $body:block) => {
+        #[test]
+        fn $name() {
+            let $param = stringify!($name);
+            let $tmpdir = &TempDir::new($param).unwrap();
+
+            let result = panic::catch_unwind(|| $body);
+
+            cancel_jobs(&format_job_name($param));
+
+            if let Err(e) = result {
+                panic::resume_unwind(e);
+            }
+        }
+    };
+}
+
+test! {
+    fn start_monitor_and_cancel(name: &str, tmp_dir: &TempDir) {
+        let mut server = mock_http_server(name, HttpServerResponses {
+            body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+            delay: 10_000,
+        });
+
+        let mut client = InProcessClient::new(format_job_name(name), tmp_dir.path().into()).unwrap();
+
+        let interval = 10_000;
+        let timeout = 10_000;
+
+        let (StartJobSuccess {guid}, mut monitor) =
+            client.start_job(
+                server.format_url(name),
+                name.into(),
+                BitsProxyUsage::Preconfig,
+                interval
+                ).unwrap();
+
+        // cancel in ~250ms
+        let _join = thread::Builder::new()
+            .spawn(move || {
+                thread::sleep(Duration::from_millis(250));
+                client.cancel_job(guid).unwrap();
+            });
+
+        let start = Instant::now();
+
+        // First immediate report
+        monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+        // ~250ms the cancel should cause an immediate disconnect (otherwise we wouldn't get
+        // an update until 10s when the transfer completes or the interval expires)
+        match monitor.get_status(timeout) {
+            Err(Error::NotConnected) => {},
+            Ok(r) => panic!("unexpected success from get_status() {:?}", r),
+            Err(e) => panic!("unexpected failure from get_status() {:?}", e),
+        }
+        assert!(start.elapsed() < Duration::from_millis(9_000));
+
+        server.shutdown();
+    }
+}
+
+test! {
+    fn start_monitor_and_complete(name: &str, tmp_dir: &TempDir) {
+        let file_path = tmp_dir.path().join(name);
+
+        let mut server = mock_http_server(name, HttpServerResponses {
+            body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+            delay: 500,
+        });
+
+        let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+        let interval = 100;
+        let timeout = 10_000;
+
+        let (StartJobSuccess {guid}, mut monitor) =
+            client.start_job(server.format_url(name), name.into(), BitsProxyUsage::Preconfig, interval).unwrap();
+
+        // get status reports until transfer finishes (~500ms)
+        let mut completed = false;
+        loop {
+            match monitor.get_status(timeout) {
+                Err(e) => {
+                    if completed {
+                        break;
+                    } else {
+                        panic!("monitor failed before completion {:?}", e);
+                    }
+                }
+                Ok(Ok(status)) => match BitsJobState::from(status.state) {
+                    BitsJobState::Connecting
+                        | BitsJobState::Transferring => {
+                            //eprintln!("{:?}", BitsJobState::from(status.state));
+                            //eprintln!("{:?}", status);
+                        }
+                    BitsJobState::Transferred => {
+                        client.complete_job(guid.clone()).unwrap();
+                        completed = true;
+                    }
+                    _ => {
+                        panic!(format!("{:?}", status));
+                    }
+                }
+                Ok(Err(e)) => panic!(format!("{:?}", e)),
+            }
+        }
+
+
+        // Verify file contents
+        let result = panic::catch_unwind(|| {
+            let mut file = File::open(file_path.clone()).unwrap();
+            let mut v = Vec::new();
+            file.read_to_end(&mut v).unwrap();
+            assert_eq!(v, name.as_bytes());
+        });
+
+        let _ = fs::remove_file(file_path);
+
+        if let Err(e) = result {
+            panic::resume_unwind(e);
+        }
+
+        // Save this for last to ensure the file is removed.
+        server.shutdown();
+    }
+}
+
+test! {
+    fn async_transferred_notification(name: &str, tmp_dir: &TempDir) {
+        let mut server = mock_http_server(name, HttpServerResponses {
+            body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+            delay: 250,
+        });
+
+        let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+        let interval = 60_000;
+        let timeout = 10_000;
+
+        let (_, mut monitor) =
+            client.start_job(server.format_url(name), name.into(), BitsProxyUsage::Preconfig, interval).unwrap();
+
+        // Start the timer now, the initial job creation may be delayed by BITS service startup.
+        let start = Instant::now();
+
+        // First immediate report
+        monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+        // Transferred notification should come when the job completes in ~250 ms, otherwise we
+        // will be stuck until timeout.
+        let status = monitor.get_status(timeout).expect("should get status update").unwrap();
+        assert!(start.elapsed() < Duration::from_millis(9_000));
+        assert_eq!(status.state, BitsJobState::Transferred);
+
+        let short_timeout = 500;
+        monitor.get_status(short_timeout).expect_err("should be disconnected");
+
+        server.shutdown();
+
+        // job will be cancelled by macro
+    }
+}
+
+test! {
+    fn change_interval(name: &str, tmp_dir: &TempDir) {
+        let mut server = mock_http_server(name, HttpServerResponses {
+            body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+            delay: 1000,
+        });
+
+        let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+        let interval = 60_000;
+        let timeout = 10_000;
+
+        let (StartJobSuccess { guid }, mut monitor) =
+            client.start_job(server.format_url(name), name.into(), BitsProxyUsage::Preconfig, interval).unwrap();
+
+        let start = Instant::now();
+
+        // reduce monitor interval in ~250ms to 500ms
+        let _join = thread::Builder::new()
+            .spawn(move || {
+                thread::sleep(Duration::from_millis(250));
+                client.set_update_interval(guid, 500).unwrap();
+            });
+
+        // First immediate report
+        monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+        // Next report should be rescheduled to 500ms by the spawned thread, otherwise no status
+        // until the original 10s interval.
+        monitor.get_status(timeout).expect("expected second status").unwrap();
+        assert!(start.elapsed() < Duration::from_millis(9_000));
+        assert!(start.elapsed() > Duration::from_millis(400));
+
+        server.shutdown();
+
+        // job will be cancelled by macro
+    }
+}
+
+test! {
+    fn async_error_notification(name: &str, tmp_dir: &TempDir) {
+        let mut server = mock_http_server(name, HttpServerResponses {
+            body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+            delay: 100,
+        });
+
+        let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+        let interval = 60_000;
+        let timeout = 10_000;
+
+        let (_, mut monitor) =
+            client.start_job(server.format_url("error_404"), name.into(), BitsProxyUsage::Preconfig, interval).unwrap();
+
+        // Start the timer now, the initial job creation may be delayed by BITS service startup.
+        let start = Instant::now();
+
+        // First immediate report
+        monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+        // Error notification should come with HEAD response in 100ms, otherwise no status until
+        // 10s interval or timeout.
+        let status = monitor.get_status(timeout).expect("should get status update").unwrap();
+        assert!(start.elapsed() < Duration::from_millis(9_000));
+        assert_eq!(status.state, BitsJobState::Error);
+
+        server.shutdown();
+
+        // job will be cancelled by macro
+    }
+}
+
+test! {
+    fn transient_error(name: &str, tmp_dir: &TempDir) {
+        let mut server = mock_http_server(name, HttpServerResponses {
+            body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+            delay: 100,
+        });
+
+        let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+        let interval = 1_000;
+        let timeout = 10_000;
+
+        let (_, mut monitor) =
+            client.start_job(server.format_url("error_500"), name.into(), BitsProxyUsage::Preconfig, interval).unwrap();
+
+        // Start the timer now, the initial job creation may be delayed by BITS service startup.
+        let start = Instant::now();
+
+        // First immediate report
+        monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+        // Transient error notification should come when the interval expires in ~1s.
+        let status = monitor.get_status(timeout).expect("should get status update").unwrap();
+        assert!(start.elapsed() > Duration::from_millis(900));
+        assert!(start.elapsed() < Duration::from_millis(9_000));
+        assert_eq!(status.state, BitsJobState::TransientError);
+
+        server.shutdown();
+
+        // job will be cancelled by macro
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/lib.rs
@@ -0,0 +1,248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! An interface for managing and monitoring BITS jobs.
+//!
+//! BITS is a Windows service for performing downloads in the background, independent from an
+//! application, usually via HTTP/HTTPS.
+//!
+//! [`BitsClient`](enum.BitsClient.html) is the main interface, used to issue commands.
+//!
+//! [`BitsMonitorClient`](enum.BitsMonitorClient.html) delivers periodic status reports about a
+//! job.
+//!
+//! Microsoft's documentation for BITS can be found at
+//! <https://docs.microsoft.com/en-us/windows/desktop/Bits/background-intelligent-transfer-service-portal>
+
+extern crate bits;
+extern crate comedy;
+extern crate failure;
+extern crate failure_derive;
+extern crate guid_win;
+
+pub mod bits_protocol;
+
+mod in_process;
+
+use std::convert;
+use std::ffi;
+
+use bits_protocol::*;
+use failure::Fail;
+
+pub use bits::status::{BitsErrorContext, BitsJobState, BitsJobTimes};
+pub use bits::{BitsJobProgress, BitsJobStatus, BitsProxyUsage};
+pub use bits_protocol::{JobError, JobStatus};
+pub use comedy::HResult;
+pub use guid_win::Guid;
+
+// These errors would come from a Local Service client but are mostly unused currently.
+// PipeError properly lives in the crate that deals with named pipes, but it isn't in use now.
+#[derive(Clone, Debug, Eq, Fail, PartialEq)]
+pub enum PipeError {
+    #[fail(display = "Pipe is not connected")]
+    NotConnected,
+    #[fail(display = "Operation timed out")]
+    Timeout,
+    #[fail(display = "Should have written {} bytes, wrote {}", _0, _1)]
+    WriteCount(usize, u32),
+    #[fail(display = "Windows API error")]
+    Api(#[fail(cause)] HResult),
+}
+
+impl convert::From<HResult> for PipeError {
+    fn from(err: HResult) -> PipeError {
+        PipeError::Api(err)
+    }
+}
+
+pub use PipeError as Error;
+
+/// A client for interacting with BITS.
+///
+/// Methods on `BitsClient` return a `Result<Result<_, XyzFailure>, Error>`. The outer `Result`
+/// is `Err` if there was a communication error in sending the associated command or receiving
+/// its response. Currently this is always `Ok` as all clients are in-process. The inner
+/// `Result` is `Err` if there was an error executing the command.
+///
+/// A single `BitsClient` can be used with multiple BITS jobs simultaneously; generally a job
+/// is not bound tightly to a client.
+///
+/// A `BitsClient` tracks all [`BitsMonitorClient`s](enum.BitsMonitorClient.html) that it started
+/// with `start_job()` or `monitor_job()`, so that the monitor can be stopped or modified.
+pub enum BitsClient {
+    // The `InProcess` variant does all BITS calls directly.
+    #[doc(hidden)]
+    InProcess(in_process::InProcessClient),
+    // Space is reserved here for the LocalService variant, which will work through an external
+    // process running as Local Service.
+}
+
+use BitsClient::InProcess;
+
+impl BitsClient {
+    /// Create an in-process `BitsClient`.
+    ///
+    /// `job_name` will be used when creating jobs, and this `BitsClient` can only be used to
+    /// manipulate jobs with that name.
+    ///
+    /// `save_path_prefix` will be prepended to the local `save_path` given to `start_job()`, it
+    /// must name an existing directory.
+    pub fn new(
+        job_name: ffi::OsString,
+        save_path_prefix: ffi::OsString,
+    ) -> Result<BitsClient, Error> {
+        Ok(InProcess(in_process::InProcessClient::new(
+            job_name,
+            save_path_prefix,
+        )?))
+    }
+
+    /// Start a job to download a single file at `url` to local path `save_path` (relative to the
+    /// `save_path_prefix` given when constructing the `BitsClient`).
+    ///
+    /// `save_path_prefix` combined with `save_path` must name a file (existing or not) in an
+    /// existing directory, which must be under the directory named by `save_path_prefix`.
+    ///
+    /// `proxy_usage` determines what proxy will be used.
+    ///
+    /// When a successful result `Ok(result)` is returned, `result.0.guid` is the id for the
+    /// new job, and `result.1` is a monitor client that can be polled for periodic updates,
+    /// returning a result approximately once per `monitor_interval_millis` milliseconds.
+    pub fn start_job(
+        &mut self,
+        url: ffi::OsString,
+        save_path: ffi::OsString,
+        proxy_usage: BitsProxyUsage,
+        monitor_interval_millis: u32,
+    ) -> Result<Result<(StartJobSuccess, BitsMonitorClient), StartJobFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client
+                .start_job(url, save_path, proxy_usage, monitor_interval_millis)
+                .map(|(success, monitor)| (success, BitsMonitorClient::InProcess(monitor)))),
+        }
+    }
+
+    /// Start monitoring the job with id `guid` approximately once per `monitor_interval_millis`
+    /// milliseconds.
+    ///
+    /// The returned `Ok(monitor)` is a monitor client to be polled for periodic updates.
+    ///
+    /// There can only be one ongoing `BitsMonitorClient` for each job associated with a given
+    /// `BitsClient`. If a monitor client already exists for the specified job, it will be stopped.
+    pub fn monitor_job(
+        &mut self,
+        guid: Guid,
+        interval_millis: u32,
+    ) -> Result<Result<BitsMonitorClient, MonitorJobFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client
+                .monitor_job(guid, interval_millis)
+                .map(BitsMonitorClient::InProcess)),
+        }
+    }
+
+    /// Suspend job `guid`.
+    pub fn suspend_job(&mut self, guid: Guid) -> Result<Result<(), SuspendJobFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client.suspend_job(guid)),
+        }
+    }
+
+    /// Resume job `guid`.
+    pub fn resume_job(&mut self, guid: Guid) -> Result<Result<(), ResumeJobFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client.resume_job(guid)),
+        }
+    }
+
+    /// Set the priority of job `guid`.
+    ///
+    /// `foreground == true` will set the priority to `BG_JOB_PRIORITY_FOREGROUND`,
+    /// `false` will use the default `BG_JOB_PRIORITY_NORMAL`.
+    /// See the Microsoft documentation for `BG_JOB_PRIORITY` for details.
+    ///
+    /// A job created by `start_job()` will be foreground priority, by default.
+    pub fn set_job_priority(
+        &mut self,
+        guid: Guid,
+        foreground: bool,
+    ) -> Result<Result<(), SetJobPriorityFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client.set_job_priority(guid, foreground)),
+        }
+    }
+
+    /// Change the update interval for an ongoing monitor of job `guid`.
+    pub fn set_update_interval(
+        &mut self,
+        guid: Guid,
+        interval_millis: u32,
+    ) -> Result<Result<(), SetUpdateIntervalFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client.set_update_interval(guid, interval_millis)),
+        }
+    }
+
+    /// Stop any ongoing monitor for job `guid`.
+    pub fn stop_update(
+        &mut self,
+        guid: Guid,
+    ) -> Result<Result<(), SetUpdateIntervalFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client.stop_update(guid)),
+        }
+    }
+
+    /// Complete the job `guid`.
+    ///
+    /// This also stops any ongoing monitor for the job.
+    pub fn complete_job(&mut self, guid: Guid) -> Result<Result<(), CompleteJobFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client.complete_job(guid)),
+        }
+    }
+
+    /// Cancel the job `guid`.
+    ///
+    /// This also stops any ongoing monitor for the job.
+    pub fn cancel_job(&mut self, guid: Guid) -> Result<Result<(), CancelJobFailure>, Error> {
+        match self {
+            InProcess(client) => Ok(client.cancel_job(guid)),
+        }
+    }
+}
+
+/// The client side of a monitor for a BITS job.
+///
+/// It is intended to be used by calling `get_status` in a loop to receive notifications about
+/// the status of a job. Because `get_status` blocks, it is recommended to run this loop on its
+/// own thread.
+pub enum BitsMonitorClient {
+    InProcess(in_process::InProcessMonitor),
+}
+
+impl BitsMonitorClient {
+    /// `get_status` will return a result approximately every `monitor_interval_millis`
+    /// milliseconds, but in case a result isn't available within `timeout_millis` milliseconds
+    /// this will return `Err(Error::Timeout)`. Any `Err` returned, including timeout, indicates
+    /// that the monitor has been stopped; the `BitsMonitorClient` should then be discarded.
+    ///
+    /// As with methods on `BitsClient`, `BitsMonitorClient::get_status()` has an inner `Result`
+    /// type which indicates an error returned from the server. Any `Err` here also indicates that
+    /// the monitor has stopped after yielding the result.
+    ///
+    /// The first time `get_status` is called it will return a status without any delay.
+    ///
+    /// If there is an error or the transfer completes, a result may be available sooner than
+    /// the monitor interval.
+    pub fn get_status(
+        &mut self,
+        timeout_millis: u32,
+    ) -> Result<Result<JobStatus, HResultMessage>, Error> {
+        match self {
+            BitsMonitorClient::InProcess(client) => client.get_status(timeout_millis),
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/lib.rs
@@ -0,0 +1,1 @@
+extern crate bits_client;
--- a/toolkit/library/gtest/rust/Cargo.toml
+++ b/toolkit/library/gtest/rust/Cargo.toml
@@ -16,16 +16,17 @@ simd-accel = ["gkrust-shared/simd-accel"
 moz_memory = ["gkrust-shared/moz_memory"]
 spidermonkey_rust = ["gkrust-shared/spidermonkey_rust"]
 cranelift_x86 = ["gkrust-shared/cranelift_x86"]
 cranelift_arm32 = ["gkrust-shared/cranelift_arm32"]
 cranelift_arm64 = ["gkrust-shared/cranelift_arm64"]
 cranelift_none = ["gkrust-shared/cranelift_none"]
 gecko_profiler = ["gkrust-shared/gecko_profiler"]
 gecko_profiler_parse_elf = ["gkrust-shared/gecko_profiler_parse_elf"]
+bitsdownload = ["gkrust-shared/bitsdownload"]
 
 [dependencies]
 bench-collections-gtest = { path = "../../../../xpcom/rust/gtest/bench-collections" }
 mp4parse-gtest = { path = "../../../../dom/media/gtest" }
 nsstring-gtest = { path = "../../../../xpcom/rust/gtest/nsstring" }
 xpcom-gtest = { path = "../../../../xpcom/rust/gtest/xpcom" }
 gkrust-shared = { path = "../../rust/shared" }
 
--- a/toolkit/library/rust/Cargo.toml
+++ b/toolkit/library/rust/Cargo.toml
@@ -16,16 +16,17 @@ simd-accel = ["gkrust-shared/simd-accel"
 moz_memory = ["gkrust-shared/moz_memory"]
 spidermonkey_rust = ["gkrust-shared/spidermonkey_rust"]
 cranelift_x86 = ["gkrust-shared/cranelift_x86"]
 cranelift_arm32 = ["gkrust-shared/cranelift_arm32"]
 cranelift_arm64 = ["gkrust-shared/cranelift_arm64"]
 cranelift_none = ["gkrust-shared/cranelift_none"]
 gecko_profiler = ["gkrust-shared/gecko_profiler"]
 gecko_profiler_parse_elf = ["gkrust-shared/gecko_profiler_parse_elf"]
+bitsdownload = ["gkrust-shared/bitsdownload"]
 
 [dependencies]
 gkrust-shared = { path = "shared" }
 mozilla-central-workspace-hack = { path = "../../../build/workspace-hack" }
 
 [dev-dependencies]
 stylo_tests = { path = "../../../servo/ports/geckolib/tests/" }
 
--- a/toolkit/library/rust/gkrust-features.mozbuild
+++ b/toolkit/library/rust/gkrust-features.mozbuild
@@ -37,8 +37,11 @@ if CONFIG['ENABLE_WASM_CRANELIFT']:
     else:
         gkrust_features += ['cranelift_none']
 
 if CONFIG['MOZ_GECKO_PROFILER']:
     gkrust_features += ['gecko_profiler']
 
 if CONFIG['MOZ_GECKO_PROFILER_PARSE_ELF']:
     gkrust_features += ['gecko_profiler_parse_elf']
+
+if CONFIG['MOZ_BITS_DOWNLOAD']:
+    gkrust_features += ['bitsdownload']
--- a/toolkit/library/rust/moz.build
+++ b/toolkit/library/rust/moz.build
@@ -16,8 +16,11 @@ RUST_TESTS = [
     'stylo_tests',
 ]
 RUST_TEST_FEATURES = gkrust_features
 
 if CONFIG['CPU_ARCH'] != 'x86':
     # malloc_size_of_derive is a build dependency, so if we are doing
     # cross-compile for x86, this may not run correctly.
     RUST_TESTS += ['malloc_size_of_derive']
+
+if CONFIG['MOZ_BITS_DOWNLOAD']:
+    RUST_TESTS += ['bits_client']
--- a/toolkit/library/rust/shared/Cargo.toml
+++ b/toolkit/library/rust/shared/Cargo.toml
@@ -28,16 +28,17 @@ gkrust_utils = { path = "../../../../xpc
 rsdparsa_capi = { path = "../../../../media/webrtc/signaling/src/sdp/rsdparsa_capi" }
 # We have these to enforce common feature sets for said crates.
 log = {version = "0.4", features = ["release_max_level_info"]}
 env_logger = {version = "0.5", default-features = false} # disable `regex` to reduce code size
 cose-c = { version = "0.1.5" }
 jsrust_shared = { path = "../../../../js/src/rust/shared", optional = true }
 arrayvec = "0.4"
 cert_storage = { path = "../../../../security/manager/ssl/cert_storage" }
+bitsdownload = { path = "../../../components/bitsdownload", optional = true }
 
 [build-dependencies]
 rustc_version = "0.2"
 
 [features]
 default = []
 bindgen = ["geckoservo/bindgen"]
 servo = ["geckoservo"]
--- a/toolkit/library/rust/shared/lib.rs
+++ b/toolkit/library/rust/shared/lib.rs
@@ -31,16 +31,18 @@ extern crate env_logger;
 extern crate u2fhid;
 extern crate gkrust_utils;
 extern crate log;
 extern crate cert_storage;
 extern crate cosec;
 extern crate rsdparsa_capi;
 #[cfg(feature = "spidermonkey_rust")]
 extern crate jsrust_shared;
+#[cfg(feature = "bitsdownload")]
+extern crate bitsdownload;
 
 extern crate arrayvec;
 
 use std::boxed::Box;
 use std::env;
 use std::ffi::{CStr, CString};
 use std::os::raw::c_char;
 use std::os::raw::c_int;
--- a/toolkit/modules/AppConstants.jsm
+++ b/toolkit/modules/AppConstants.jsm
@@ -182,16 +182,23 @@ this.AppConstants = Object.freeze({
 
   MOZ_MAINTENANCE_SERVICE:
 #ifdef MOZ_MAINTENANCE_SERVICE
   true,
 #else
   false,
 #endif
 
+  MOZ_BITS_DOWNLOAD:
+#ifdef MOZ_BITS_DOWNLOAD
+  true,
+#else
+  false,
+#endif
+
   DEBUG:
 #ifdef DEBUG
   true,
 #else
   false,
 #endif
 
   ASAN:
--- a/toolkit/moz.configure
+++ b/toolkit/moz.configure
@@ -1263,16 +1263,30 @@ option('--enable-maintenance-service',
 
 set_define('MOZ_MAINTENANCE_SERVICE',
            depends_if('--enable-maintenance-service',
                       when=target_is_windows)(lambda _: True))
 set_config('MOZ_MAINTENANCE_SERVICE',
            depends_if('--enable-maintenance-service',
                       when=target_is_windows)(lambda _: True))
 
+# BITS download (Windows only)
+# ==============================================================
+
+option('--enable-bits-download',
+       when=target_is_windows, default=target_is_windows,
+       help='{Enable|Disable} building BITS download support')
+
+set_define('MOZ_BITS_DOWNLOAD',
+           depends_if('--enable-bits-download',
+                      when=target_is_windows)(lambda _: True))
+set_config('MOZ_BITS_DOWNLOAD',
+           depends_if('--enable-bits-download',
+                      when=target_is_windows)(lambda _: True))
+
 # Bundled fonts on desktop platform
 # ==============================================================
 
 @depends(target)
 def bundled_fonts_default(target):
     return target.os == 'WINNT' or target.kernel == 'Linux'
 
 @depends(build_project)