diff --git a/Cargo.lock b/Cargo.lock
index a918f9f5c95..4958f50b1e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -24,6 +24,17 @@ dependencies = [
"memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "ammonia"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "html5ever 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "maplit 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tendril 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "antidote"
version = "1.0.0"
@@ -80,6 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
name = "cargo-registry"
version = "0.1.0"
dependencies = [
+ "ammonia 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bufstream 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"cargo-registry-s3 0.1.0",
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -109,13 +121,15 @@ dependencies = [
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"oauth2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pulldown-cmark 0.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2-diesel 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tar 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -123,11 +137,11 @@ dependencies = [
[[package]]
name = "cargo_metadata"
-version = "0.2.1"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "serde 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -166,7 +180,7 @@ name = "clippy"
version = "0.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "cargo_metadata 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cargo_metadata 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"clippy_lints 0.0.142 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -181,8 +195,8 @@ dependencies = [
"quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -367,6 +381,14 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "debug_unreachable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "derive-error-chain"
version = "0.10.1"
@@ -466,6 +488,15 @@ name = "foreign-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "futf"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "futures"
version = "0.1.14"
@@ -494,9 +525,21 @@ name = "hex"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "html5ever"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "markup5ever 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "idna"
-version = "0.1.2"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -584,6 +627,29 @@ name = "log"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "maplit"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "markup5ever"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string_cache 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tendril 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "matches"
version = "0.1.6"
@@ -696,6 +762,40 @@ name = "percent-encoding"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "phf"
+version = "0.7.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.7.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.7.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.7.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.9"
@@ -709,6 +809,19 @@ dependencies = [
"vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "precomputed-hash"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "pulldown-cmark"
+version = "0.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "quine-mc_cluskey"
version = "0.2.4"
@@ -769,7 +882,7 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.1.24"
+version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -883,12 +996,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
-version = "1.0.9"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_derive"
-version = "1.0.9"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -913,9 +1026,14 @@ dependencies = [
"dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "siphasher"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "socket2"
version = "0.2.1"
@@ -928,6 +1046,36 @@ dependencies = [
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "string_cache"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "precomputed-hash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "string_cache_shared"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "syn"
version = "0.11.11"
@@ -946,6 +1094,26 @@ dependencies = [
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "tar"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "filetime 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "xattr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "tendril"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "futf 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "utf-8 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "thread-id"
version = "2.0.0"
@@ -979,7 +1147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -988,7 +1156,7 @@ name = "toml"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "serde 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1009,6 +1177,14 @@ name = "unicode-xid"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "unreachable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "unreachable"
version = "1.0.0"
@@ -1027,11 +1203,19 @@ name = "url"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "utf-8"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "utf8-ranges"
version = "0.1.3"
@@ -1071,9 +1255,18 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "xattr"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[metadata]
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
"checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699"
+"checksum ammonia 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a84d348f2d98bb52f7e8d896c3ce5b6bbf66a040dd334c9cfe1503b6d8e7d15e"
"checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5"
"checksum backtrace 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "72f9b4182546f4b04ebc4ab7f84948953a118bd6021a1b6a6c909e3e94f6be76"
"checksum backtrace-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3a0d842ea781ce92be2bf78a9b38883948542749640b8378b3b2f03d1fd9f1ff"
@@ -1081,7 +1274,7 @@ dependencies = [
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum bufstream 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f2f382711e76b9de6c744cc00d0497baba02fb00a787f088c879f01d09468e32"
"checksum byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff81738b726f5d099632ceaffe7fb65b90212e8dce59d518729e7e8634032d3d"
-"checksum cargo_metadata 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5d84cb53c78e573aa126a4b9f963fdb2629f8183b26e235da08bb36dc7381162"
+"checksum cargo_metadata 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "be1057b8462184f634c3a208ee35b0f935cfd94b694b26deadccd98732088d7b"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
"checksum chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c20ebe0b2b08b0aeddba49c609fe7957ba2e33449882cb186a180bc60682fa9"
"checksum civet 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6263e7af767a5bf9e4d3d0a6c3ceb5f3940ec85cf2fbfee59024b8a264be180f"
@@ -1106,6 +1299,7 @@ dependencies = [
"checksum curl 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6689276ab61f97c660669a5ecc117c36875dfc1ba301c986b16c653415bdf9d7"
"checksum curl-sys 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d5481162dc4f424d088581db2f979fa7d4c238fe9794595de61d8d7522e277de"
"checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
+"checksum debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a032eac705ca39214d169f83e3d3da290af06d8d1d344d1baad2fd002dca4b3"
"checksum derive-error-chain 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c9ca9ade651388daad7c993f005d0d20c4f6fe78c1cdc93e95f161c6f5ede4a"
"checksum diesel 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d7302a01c06bd94e619661c44528375a068e34717608d0d1c93bf75428da142a"
"checksum diesel_codegen 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "044abc50a0ee67e195b0ae95c9ffe6903748434294636dd67808301b7df1902f"
@@ -1118,11 +1312,13 @@ dependencies = [
"checksum filetime 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "5363ab8e4139b8568a6237db5248646e5a8a2f89bd5ccb02092182b11fd3e922"
"checksum flate2 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)" = "36df0166e856739905cd3d7e0b210fe818592211a008862599845e012d8d304c"
"checksum foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e4056b9bd47f8ac5ba12be771f77a0dae796d1bbaaf5fd0b9c2d38b69b8a29d"
+"checksum futf 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "51f93f3de6ba1794dcd5810b3546d004600a59a98266487c8407bc4b24e398f3"
"checksum futures 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "4b63a4792d4f8f686defe3b39b92127fea6344de5d38202b2ee5a11bbbf29d6a"
"checksum gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)" = "120d07f202dcc3f72859422563522b66fe6463a4c513df062874daad05f85f0a"
"checksum git2 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa01936ac96555c083c0e8553f672616274408d9d3fc5b8696603fbf63ff43ee"
"checksum hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa"
-"checksum idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2233d4940b1f19f0418c158509cd7396b8d70a5db5705ce410914dc8fa603b37"
+"checksum html5ever 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a49d5001dd1bddf042ea41ed4e0a671d50b1bf187e66b349d7ec613bdce4ad90"
+"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
"checksum itertools 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "772a0928a97246167d59a2a4729df5871f1327ab8b36fd24c4224b229cb47b99"
"checksum itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2f404fbc66fd9aac13e998248505e7ecb2ad8e44ab6388684c5fb11c6c251c"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
@@ -1133,6 +1329,9 @@ dependencies = [
"checksum libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "3fdd64ef8ee652185674455c1d450b83cbc8ad895625d543b5324d923f82e4d8"
"checksum license-exprs 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "20e170a9f8785c9bb07576397a605ac453332e833a5eb5686cd4dcbb39cc1a66"
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
+"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+"checksum maplit 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "be384c560e0c3ad868b590ffb88d2c0a1effde6f59885234e4ea811c1202bfea"
+"checksum markup5ever 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5050cc22c1d567a9b99bcbe9ffbd8f3127b3d146994105480240dc8ba4f4f2ef"
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20"
"checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4"
@@ -1147,8 +1346,14 @@ dependencies = [
"checksum openssl-probe 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d98df0270d404ccd3c050a41d579c52d1db15375168bb3471e04ec0f5f378daf"
"checksum openssl-sys 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)" = "236c718c2e2c2b58a546d86ffea5194400bb15dbe01ca85325ffd357b03cf66c"
"checksum percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de154f638187706bde41d9b4738748933d64e6b37bdbffc0b47a97d16a6ae356"
+"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc"
+"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f"
+"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03"
+"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2"
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
"checksum pq-sys 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4dfb5e575ef93a1b7b2a381d47ba7c5d4e4f73bff37cee932195de769aad9a54"
+"checksum precomputed-hash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf1fc3616b3ef726a847f2cd2388c646ef6a1f1ba4835c2629004da48184150"
+"checksum pulldown-cmark 0.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "378e941dbd392c101f2cb88097fa4d7167bc421d4b88de3ff7dbee503bc3233b"
"checksum quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "07589615d719a60c8dd8a4622e7946465dfef20d1a428f969e3443e7386d5f45"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum r2d2 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6c665a538b218e1620093be6643b375d3321837bfc1b30aa18757b7c6546d2ca"
@@ -1156,7 +1361,7 @@ dependencies = [
"checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d"
"checksum rayon 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a77c51c07654ddd93f6cb543c7a849863b03abc7e82591afda6dc8ad4ac3ac4a"
"checksum rayon-core 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7febc28567082c345f10cddc3612c6ea020fc3297a1977d472cf9fdb73e6e493"
-"checksum redox_syscall 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "9aa093607d28cfd65f317edeeefb6749be428eacc8decd1c5f8c0fbcc327aff5"
+"checksum redox_syscall 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "9df6a71a1e67be2104410736b2389fb8e383c1d7e9e792d629ff13c02867147a"
"checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
"checksum regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1731164734096285ec2a5ec7fea5248ae2f5485b3feeb0115af4fda2183b2d1b"
"checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
@@ -1172,13 +1377,19 @@ dependencies = [
"checksum semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a3186ec9e65071a2095434b1f5bb24838d4e8e130f584c790f6033c79943537"
"checksum semver-parser 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fff3c9c5a54636ab95acd8c1349926e04cb1eb8cd70b5adced8a1d1f703a67"
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
-"checksum serde 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6a7c6b751a2e8d5df57a5ff71b5b4fc8aaee9ee28ff1341d640dd130bb5f4f7a"
-"checksum serde_derive 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "2f6ca58905ebd3c3b285a8a6d4f3ac92b92c0d7951d5649b1bdd212549c06639"
+"checksum serde 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "433d7d9f8530d5a939ad5e0e72a6243d2e42a24804f70bf592c679363dcacb2f"
+"checksum serde_derive 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "7b707cf0d4cab852084f573058def08879bb467fda89d99052485e7d00edd624"
"checksum serde_derive_internals 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37aee4e0da52d801acfbc0cc219eb1eda7142112339726e427926a6f6ee65d3a"
"checksum serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "48b04779552e92037212c3615370f6bd57a40ebba7f20e554ff9f55e41a69a7b"
+"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
"checksum socket2 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12cdbddbaa27bf94cc194b8e37f5811db6fe83cea96cf99cf1f8e92b65a41371"
+"checksum string_cache 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2fa69b90c5398217fb0414706d1becea9325ad21ed5d87bd6dda82127911f324"
+"checksum string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "479cde50c3539481f33906a387f2bd17c8e87cb848c35b6021d41fb81ff9b4d7"
+"checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
+"checksum tar 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "281285b717926caa919ad905ef89c63d75805c7d89437fb873100925a53f2b1b"
+"checksum tendril 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1b72f8e2f5b73b65c315b1a70c730f24b9d7a25f39e98de8acbe2bb795caea"
"checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03"
"checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5"
"checksum thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14"
@@ -1187,9 +1398,11 @@ dependencies = [
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
+"checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91"
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
"checksum untrusted 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6b65243989ef6aacd9c0d6bd2b822765c3361d8ed352185a6f3a41f3a718c673"
"checksum url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb819346883532a271eb626deb43c4a1bb4c4dd47c519bd78137c3e72a4fe27"
+"checksum utf-8 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b6f923c601c7ac48ef1d66f7d5b5b2d9a7ba9c51333ab75a3ddf8d0309185a56"
"checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"
"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
"checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
@@ -1197,3 +1410,4 @@ dependencies = [
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+"checksum xattr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "5f04de8a1346489a2f9e9bd8526b73d135ec554227b17568456e86aa35b6f3fc"
diff --git a/Cargo.toml b/Cargo.toml
index e4f05043b3d..32bf330472e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ git2 = "0.6.4"
flate2 = "0.2"
semver = "0.5"
url = "1.2.1"
+tar = "0.4.13"
r2d2 = "0.7.0"
openssl = "0.9.9"
@@ -47,6 +48,8 @@ serde_derive = "1.0.0"
serde = "1.0.0"
clippy = { version = "=0.0.142", optional = true }
chrono = "0.4.0"
+pulldown-cmark = { version = "0.0.15", default-features = false }
+ammonia = "0.5.0"
conduit = "0.8"
conduit-conditional-get = "0.8"
diff --git a/app/components/crate-readme.js b/app/components/crate-readme.js
new file mode 100644
index 00000000000..eb18c70bc30
--- /dev/null
+++ b/app/components/crate-readme.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ rendered: '',
+ didRender() {
+ this._super(...arguments);
+ this.$('pre > code').each(function() {
+ window.Prism.highlightElement(this);
+ });
+ }
+});
diff --git a/app/models/version.js b/app/models/version.js
index fec6f6c3f39..3da95b4300d 100644
--- a/app/models/version.js
+++ b/app/models/version.js
@@ -4,6 +4,7 @@ import DS from 'ember-data';
export default DS.Model.extend({
num: DS.attr('string'),
dl_path: DS.attr('string'),
+ readme_path: DS.attr('string'),
created_at: DS.attr('date'),
updated_at: DS.attr('date'),
downloads: DS.attr('number'),
diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js
index e6a0be8e299..e6c2f189859 100644
--- a/app/routes/crate/version.js
+++ b/app/routes/crate/version.js
@@ -99,9 +99,28 @@ export default Route.extend({
`Version '${params.version_num}' of crate '${crate.get('name')}' does not exist`);
}
- return version ||
+ const result = version ||
versions.find(version => version.get('num') === maxVersion) ||
versions.objectAt(0);
+ if (result.get('readme_path')) {
+ this.get('ajax').request(result.get('readme_path'))
+ .then((r) => this.get('ajax').raw(r.url, {
+ method: 'GET',
+ dataType: 'html',
+ headers: {
+ // We need to force the Accept header, otherwise crates.io won't return
+ // the readme file when not using S3.
+ Accept: '*/*',
+ },
+ }))
+ .then((r) => {
+ crate.set('readme', r.payload);
+ })
+ .catch(() => {
+ crate.set('readme', null);
+ });
+ }
+ return result;
});
},
diff --git a/app/styles/crate.scss b/app/styles/crate.scss
index 9a39f5c60ee..b590aef5cf3 100644
--- a/app/styles/crate.scss
+++ b/app/styles/crate.scss
@@ -208,6 +208,7 @@
.docs {
@include flex(7);
padding-right: 40px;
+ max-width: 600px;
}
.authorship {
@include flex(3);
@@ -302,6 +303,13 @@
color: red;
}
}
+ .crate-readme {
+ line-height: 1.5;
+
+ pre {
+ overflow-x: scroll;
+ }
+ }
.last-update {
color: $main-color-light;
font-size: 90%;
diff --git a/app/templates/components/crate-readme.hbs b/app/templates/components/crate-readme.hbs
new file mode 100644
index 00000000000..624ded4c91a
--- /dev/null
+++ b/app/templates/components/crate-readme.hbs
@@ -0,0 +1 @@
+{{{rendered}}}
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs
index f3dcc984911..ea0a568db35 100644
--- a/app/templates/crate/version.hbs
+++ b/app/templates/crate/version.hbs
@@ -28,13 +28,28 @@
{{#if crate.homepage}}
- - Homepage
+ - Homepage
+ {{/if}}
+ {{#if crate.wiki}}
+ - Wiki
+ {{/if}}
+ {{#if crate.mailing_list}}
+ - Mailing list
{{/if}}
{{#if crate.documentation}}
- - Documentation
+ - Documentation
{{/if}}
{{#if crate.repository}}
- - Repository
+ - Repository
+ {{/if}}
+ {{#if crate.reverse_dependencies}}
+ -
+ {{#link-to 'crate.reverse_dependencies' (query-params dependency=crate.crate_id)}}
+ Dependent crates
+ {{/link-to}}
+
+ {{else}}
+ - No dependent crates
{{/if}}
@@ -54,12 +69,6 @@
{{else}}
- {{#if crate.description}}
-
-
About This Package
-
{{ crate.description }}
-
- {{/if}}
Cargo.toml
{{ crate.name }} = "{{ currentVersion.num }}"
@@ -82,6 +91,11 @@
{{/if}}
+ {{#if crate.readme}}
+
+ {{crate-readme rendered=crate.readme}}
+
+ {{/if}}
@@ -166,88 +180,54 @@
{{/each}}
-
-
-
-
- {{#if anyLinks}}
-
-
Links
-
- {{#if crate.homepage}}
- - Homepage
- {{/if}}
- {{#if crate.wiki}}
- - Wiki
- {{/if}}
- {{#if crate.mailing_list}}
- - Mailing list
- {{/if}}
- {{#if crate.documentation}}
- - Documentation
- {{/if}}
- {{#if crate.repository}}
- - Repository
- {{/if}}
- {{#if crate.reverse_dependencies}}
- -
- {{#link-to 'crate.reverse_dependencies' (query-params dependency=crate.crate_id)}}
- Dependent crates
- {{/link-to}}
-
- {{else}}
- - No dependent crates
- {{/if}}
-
-
- {{/if}}
+
+
Versions
+
+ {{#each smallSortedVersions as |version|}}
+ -
+ {{#link-to 'crate.version' version.num}}
+ {{ version.num }}
+ {{/link-to}}
+ {{moment-format version.created_at 'll'}}
+ {{#if version.yanked}}
+ yanked
+ {{/if}}
+
+ {{/each}}
+
+
+ {{#if hasMoreVersions}}
+ {{#link-to 'crate.versions' crate}}
+ show all {{ crate.versions.length }} versions
+ {{/link-to}}
+ {{/if}}
+
+
-
-
Dependencies
-
- {{#each currentDependencies as |dep|}}
- {{link-to-dep dep=dep}}
- {{else}}
- - None
- {{/each}}
-
-
+
+
Dependencies
+
+ {{#each currentDependencies as |dep|}}
+ {{link-to-dep dep=dep}}
+ {{else}}
+ - None
+ {{/each}}
+
+
- {{#if currentDevDependencies}}
-
-
Dev-Dependencies
-
- {{#each currentDevDependencies as |dep|}}
- {{link-to-dep dep=dep}}
- {{/each}}
-
+ {{#if currentDevDependencies}}
+
+
Dev-Dependencies
+
+ {{#each currentDevDependencies as |dep|}}
+ {{link-to-dep dep=dep}}
+ {{/each}}
+
+
+ {{/if}}
+
- {{/if}}
-
-
-
Versions
-
- {{#each smallSortedVersions as |version|}}
- -
- {{#link-to 'crate.version' version.num}}
- {{ version.num }}
- {{/link-to}}
- {{moment-format version.created_at 'll'}}
- {{#if version.yanked}}
- yanked
- {{/if}}
-
- {{/each}}
-
-
- {{#if hasMoreVersions}}
- {{#link-to 'crate.versions' crate}}
- show all {{ crate.versions.length }} versions
- {{/link-to}}
- {{/if}}
-
-
@@ -256,14 +236,14 @@
{{svg-jar "download"}}
- {{ format-num downloadsContext.downloads }}
+ {{format-num downloadsContext.downloads}}
Downloads all time
{{svg-jar "crate"}}
- {{ crate.versions.length }}
+ {{crate.versions.length}}
Versions published
diff --git a/ember-cli-build.js b/ember-cli-build.js
index 427a8da2746..5e987af7ed1 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -5,10 +5,31 @@
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
module.exports = function(defaults) {
+ const highlightedLanguages = [
+ 'bash',
+ 'clike',
+ 'glsl',
+ 'go',
+ 'ini',
+ 'javascript',
+ 'json',
+ 'markup',
+ 'protobuf',
+ 'ruby',
+ 'rust',
+ 'scss',
+ 'sql',
+ 'yaml'
+ ];
+
let app = new EmberApp(defaults, {
babel6: {
plugins: ['transform-object-rest-spread'],
},
+ 'ember-prism': {
+ theme: 'twilight',
+ components: highlightedLanguages,
+ }
});
// Use `app.import` to add additional libraries to the generated
diff --git a/package-lock.json b/package-lock.json
index 88024e3203e..f6ccc2e7b01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6372,6 +6372,56 @@
"ember-cli-htmlbars": "2.0.2"
}
},
+ "ember-prism": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/ember-prism/-/ember-prism-0.1.0.tgz",
+ "integrity": "sha1-JWz25nDj/9B4OXa58nSuhLU8ExQ=",
+ "dev": true,
+ "requires": {
+ "ember-cli-babel": "6.6.0",
+ "ember-cli-htmlbars": "1.3.4",
+ "ember-cli-node-assets": "0.2.2",
+ "prismjs": "1.6.0"
+ },
+ "dependencies": {
+ "ember-cli-htmlbars": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/ember-cli-htmlbars/-/ember-cli-htmlbars-1.3.4.tgz",
+ "integrity": "sha512-5lycG6z35QHr3WZF1OkVvT+N/GGAVuemtM6m8NUgBWoeA2TqOgPFRcI0eRqoLA0HAfe0R2MReKmMI7y1LEM1+w==",
+ "dev": true,
+ "requires": {
+ "broccoli-persistent-filter": "1.4.2",
+ "ember-cli-version-checker": "1.3.1",
+ "hash-for-dep": "1.1.2",
+ "json-stable-stringify": "1.0.1",
+ "strip-bom": "2.0.0"
+ }
+ },
+ "ember-cli-node-assets": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/ember-cli-node-assets/-/ember-cli-node-assets-0.2.2.tgz",
+ "integrity": "sha1-0tVWJufMZhn4gtf+VXUfkmYCJwg=",
+ "dev": true,
+ "requires": {
+ "broccoli-funnel": "1.2.0",
+ "broccoli-merge-trees": "1.2.4",
+ "broccoli-source": "1.1.0",
+ "debug": "2.6.8",
+ "lodash": "4.17.4",
+ "resolve": "1.3.3"
+ }
+ },
+ "ember-cli-version-checker": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/ember-cli-version-checker/-/ember-cli-version-checker-1.3.1.tgz",
+ "integrity": "sha1-C8LRNMgwFC2mS/lieg7e0QthrnI=",
+ "dev": true,
+ "requires": {
+ "semver": "5.3.0"
+ }
+ }
+ }
+ },
"ember-qunit": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/ember-qunit/-/ember-qunit-2.1.4.tgz",
@@ -11584,6 +11634,15 @@
"integrity": "sha1-xDjKLKM+OSdnHbSracDlL5NqTw8=",
"dev": true
},
+ "prismjs": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.6.0.tgz",
+ "integrity": "sha1-EY2V+3pm26InLjQ7NF9SNmWds2U=",
+ "dev": true,
+ "requires": {
+ "clipboard": "1.7.1"
+ }
+ },
"private": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz",
diff --git a/package.json b/package.json
index e164b315d03..85f2f1206a1 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"ember-moment": "^7.3.1",
"ember-normalize": "^1.0.0",
"ember-page-title": "^3.2.1",
+ "ember-prism": "^0.1.0",
"ember-resolver": "^4.1.0",
"ember-router-scroll": "^0.2.0",
"ember-source": "~2.13.3",
diff --git a/src/bin/render-readmes.rs b/src/bin/render-readmes.rs
new file mode 100644
index 00000000000..00000dc3843
--- /dev/null
+++ b/src/bin/render-readmes.rs
@@ -0,0 +1,249 @@
+// Iterates over every crate versions ever uploaded and (re-)renders their
+// readme using the Markdown renderer from the cargo_registry crate.
+//
+// Warning: this can take a lot of time.
+//
+// Usage:
+// cargo run --bin render-readmes [page-size: optional = 25]
+// The page-size argument dictate how much versions should be queried and processed at once.
+
+#![deny(warnings)]
+
+#[macro_use]
+extern crate serde_derive;
+
+extern crate cargo_registry;
+extern crate curl;
+extern crate diesel;
+extern crate flate2;
+extern crate s3;
+extern crate tar;
+extern crate time;
+extern crate toml;
+extern crate url;
+
+use curl::easy::{Easy, List};
+use diesel::prelude::*;
+use flate2::read::GzDecoder;
+use std::env;
+use std::io::{Cursor, Read};
+use std::path::Path;
+use std::thread;
+use tar::Archive;
+use url::Url;
+
+use cargo_registry::{Config, Version};
+use cargo_registry::version::EncodableVersion;
+use cargo_registry::schema::*;
+use cargo_registry::render::markdown_to_html;
+
+const DEFAULT_PAGE_SIZE: i64 = 25;
+
+fn main() {
+ let config: Config = Default::default();
+ let conn = cargo_registry::db::connect_now().unwrap();
+ let versions_count = versions::table
+ .select(versions::all_columns)
+ .count()
+ .get_result::
(&conn)
+ .expect("error counting versions");
+ let page_size = match env::args().nth(1) {
+ None => DEFAULT_PAGE_SIZE,
+ Some(s) => s.parse::().unwrap_or(DEFAULT_PAGE_SIZE),
+ };
+ let pages = if versions_count % page_size == 0 {
+ versions_count / page_size
+ } else {
+ versions_count / page_size + 1
+ };
+ for current_page in 0..pages {
+ let versions: Vec = versions::table
+ .inner_join(crates::table)
+ .select((versions::all_columns, crates::name))
+ .limit(page_size)
+ .offset(current_page * page_size)
+ .load::<(Version, String)>(&conn)
+ .expect("error loading versions")
+ .into_iter()
+ .map(|(version, crate_name)| version.encodable(&crate_name))
+ .collect();
+ let mut tasks = Vec::with_capacity(page_size as usize);
+ for version in versions {
+ let config = config.clone();
+ let handle = thread::spawn(move || {
+ println!("[{}-{}] Rendering README...", version.krate, version.num);
+ let readme = get_readme(&config, &version);
+ if readme.is_none() {
+ return;
+ }
+ let readme = readme.unwrap();
+ let readme_path = format!(
+ "readmes/{}/{}-{}.html",
+ version.krate,
+ version.krate,
+ version.num
+ );
+ let readme_len = readme.len();
+ let mut body = Cursor::new(readme.into_bytes());
+ config
+ .uploader
+ .upload(
+ Easy::new(),
+ &readme_path,
+ &mut body,
+ "text/html",
+ readme_len as u64,
+ )
+ .expect(&format!(
+ "[{}-{}] Couldn't upload file to S3",
+ version.krate,
+ version.num
+ ));
+ });
+ tasks.push(handle);
+ }
+ for handle in tasks {
+ if let Err(err) = handle.join() {
+ println!("Thead panicked: {:?}", err);
+ }
+ }
+ }
+}
+
+/// Renders the readme of an uploaded crate version.
+fn get_readme(config: &Config, version: &EncodableVersion) -> Option {
+ let mut handle = Easy::new();
+ let location = match config.uploader.crate_location(&version.krate, &version.num) {
+ Some(l) => l,
+ None => return None,
+ };
+ let date = time::now().rfc822z().to_string();
+ let url = Url::parse(&location).expect(&format!(
+ "[{}-{}] Couldn't parse crate URL",
+ version.krate,
+ version.num
+ ));
+
+ let mut headers = List::new();
+ headers
+ .append(&format!("Host: {}", url.host().unwrap()))
+ .unwrap();
+ headers.append(&format!("Date: {}", date)).unwrap();
+
+ handle.url(url.as_str()).unwrap();
+ handle.get(true).unwrap();
+ handle.http_headers(headers).unwrap();
+
+ let mut response = Vec::new();
+ {
+ let mut req = handle.transfer();
+ req.write_function(|data| {
+ response.extend(data);
+ Ok(data.len())
+ }).unwrap();
+ if let Err(err) = req.perform() {
+ println!(
+ "[{}-{}] Unable to fetch crate: {}",
+ version.krate,
+ version.num,
+ err
+ );
+ return None;
+ }
+ }
+ if handle.response_code().unwrap() != 200 {
+ let response = String::from_utf8_lossy(&response);
+ println!(
+ "[{}-{}] Failed to get a 200 response: {}",
+ version.krate,
+ version.num,
+ response
+ );
+ return None;
+ }
+ let reader = Cursor::new(response);
+ let reader = GzDecoder::new(reader).expect(&format!(
+ "[{}-{}] Invalid gzip header",
+ version.krate,
+ version.num
+ ));
+ let mut archive = Archive::new(reader);
+ let mut entries = archive.entries().expect(&format!(
+ "[{}-{}] Invalid tar archive entries",
+ version.krate,
+ version.num
+ ));
+ let manifest: Manifest = {
+ let path = format!("{}-{}/Cargo.toml", version.krate, version.num);
+ let contents = find_file_by_path(&mut entries, Path::new(&path), &version);
+ toml::from_str(&contents).expect(&format!(
+ "[{}-{}] Syntax error in manifest file",
+ version.krate,
+ version.num
+ ))
+ };
+ if manifest.package.readme.is_none() {
+ return None;
+ }
+ let rendered = {
+ let path = format!(
+ "{}-{}/{}",
+ version.krate,
+ version.num,
+ manifest.package.readme.unwrap()
+ );
+ let contents = find_file_by_path(&mut entries, Path::new(&path), &version);
+ markdown_to_html(&contents).expect(&format!(
+ "[{}-{}] Couldn't render README",
+ version.krate,
+ version.num
+ ))
+ };
+ return Some(rendered);
+ #[derive(Deserialize)]
+ struct Package {
+ readme: Option,
+ }
+ #[derive(Deserialize)]
+ struct Manifest {
+ package: Package,
+ }
+}
+
+/// Search an entry by its path in a Tar archive.
+fn find_file_by_path(
+ mut entries: &mut tar::Entries,
+ path: &Path,
+ version: &EncodableVersion,
+) -> String {
+ let mut file = entries
+ .find(|entry| match *entry {
+ Err(_) => false,
+ Ok(ref file) => {
+ let filepath = match file.path() {
+ Ok(p) => p,
+ Err(_) => return false,
+ };
+ return filepath == path;
+ }
+ })
+ .expect(&format!(
+ "[{}-{}] couldn't open file: {}",
+ version.krate,
+ version.num,
+ path.display()
+ ))
+ .expect(&format!(
+ "[{}-{}] file is not present: {}",
+ version.krate,
+ version.num,
+ path.display()
+ ));
+ let mut contents = String::new();
+ file.read_to_string(&mut contents).expect(&format!(
+ "[{}-{}] Couldn't read file contents",
+ version.krate,
+ version.num
+ ));
+ contents
+}
diff --git a/src/bin/server.rs b/src/bin/server.rs
index 9245acea5ec..306d543492c 100644
--- a/src/bin/server.rs
+++ b/src/bin/server.rs
@@ -7,11 +7,10 @@ extern crate git2;
extern crate env_logger;
extern crate s3;
-use cargo_registry::{env, Env, Uploader, Replica};
+use cargo_registry::{env, Env};
use civet::Server;
use std::env;
use std::fs::{self, File};
-use std::path::PathBuf;
use std::sync::Arc;
use std::sync::mpsc::channel;
@@ -19,25 +18,25 @@ use std::sync::mpsc::channel;
fn main() {
// Initialize logging
env_logger::init().unwrap();
+ let config: cargo_registry::Config = Default::default();
// If there isn't a git checkout containing the crate index repo at the path specified
// by `GIT_REPO_CHECKOUT`, delete that directory and clone the repo specified by `GIT_REPO_URL`
// into that directory instead. Uses the credentials specified in `GIT_HTTP_USER` and
// `GIT_HTTP_PWD` via the `cargo_registry::git::credentials` function.
let url = env("GIT_REPO_URL");
- let checkout = PathBuf::from(env("GIT_REPO_CHECKOUT"));
- let repo = match git2::Repository::open(&checkout) {
+ let repo = match git2::Repository::open(&config.git_repo_checkout) {
Ok(r) => r,
Err(..) => {
- let _ = fs::remove_dir_all(&checkout);
- fs::create_dir_all(&checkout).unwrap();
+ let _ = fs::remove_dir_all(&config.git_repo_checkout);
+ fs::create_dir_all(&config.git_repo_checkout).unwrap();
let mut cb = git2::RemoteCallbacks::new();
cb.credentials(cargo_registry::git::credentials);
let mut opts = git2::FetchOptions::new();
opts.remote_callbacks(cb);
git2::build::RepoBuilder::new()
.fetch_options(opts)
- .clone(&url, &checkout)
+ .clone(&url, &config.git_repo_checkout)
.unwrap()
}
};
@@ -48,97 +47,6 @@ fn main() {
cfg.set_str("user.name", "bors").unwrap();
cfg.set_str("user.email", "bors@rust-lang.org").unwrap();
- let api_protocol = String::from("https");
-
- let mirror = if env::var("MIRROR").is_ok() {
- Replica::ReadOnlyMirror
- } else {
- Replica::Primary
- };
-
- let heroku = env::var("HEROKU").is_ok();
- let cargo_env = if heroku {
- Env::Production
- } else {
- Env::Development
- };
-
- let uploader = match (cargo_env, mirror) {
- (Env::Production, Replica::Primary) => {
- // `env` panics if these vars are not set, and in production for a primary instance,
- // that's what we want since we don't want to be able to start the server if the server
- // doesn't know where to upload crates.
- Uploader::S3 {
- bucket: s3::Bucket::new(
- env("S3_BUCKET"),
- env::var("S3_REGION").ok(),
- env("S3_ACCESS_KEY"),
- env("S3_SECRET_KEY"),
- &api_protocol,
- ),
- proxy: None,
- }
- }
- (Env::Production, Replica::ReadOnlyMirror) => {
- // Read-only mirrors don't need access key or secret key since by definition,
- // they'll only need to read from a bucket, not upload.
- //
- // Read-only mirrors might have access key or secret key, so use them if those
- // environment variables are set.
- //
- // Read-only mirrors definitely need bucket though, so that they know where
- // to serve crate files from.
- Uploader::S3 {
- bucket: s3::Bucket::new(
- env("S3_BUCKET"),
- env::var("S3_REGION").ok(),
- env::var("S3_ACCESS_KEY").unwrap_or(String::new()),
- env::var("S3_SECRET_KEY").unwrap_or(String::new()),
- &api_protocol,
- ),
- proxy: None,
- }
- }
- // In Development mode, either running as a primary instance or a read-only mirror
- _ => {
- if env::var("S3_BUCKET").is_ok() {
- // If we've set the `S3_BUCKET` variable to any value, use all of the values
- // for the related S3 environment variables and configure the app to upload to
- // and read from S3 like production does. All values except for bucket are
- // optional, like production read-only mirrors.
- println!("Using S3 uploader");
- Uploader::S3 {
- bucket: s3::Bucket::new(
- env("S3_BUCKET"),
- env::var("S3_REGION").ok(),
- env::var("S3_ACCESS_KEY").unwrap_or(String::new()),
- env::var("S3_SECRET_KEY").unwrap_or(String::new()),
- &api_protocol,
- ),
- proxy: None,
- }
- } else {
- // If we don't set the `S3_BUCKET` variable, we'll use a development-only
- // uploader that makes it possible to run and publish to a locally-running
- // crates.io instance without needing to set up an account and a bucket in S3.
- println!("Using local uploader, crate files will be in the dist directory");
- Uploader::Local
- }
- }
- };
-
- let config = cargo_registry::Config {
- uploader: uploader,
- session_key: env("SESSION_KEY"),
- git_repo_checkout: checkout,
- gh_client_id: env("GH_CLIENT_ID"),
- gh_client_secret: env("GH_CLIENT_SECRET"),
- db_url: env("DATABASE_URL"),
- env: cargo_env,
- max_upload_size: 10 * 1024 * 1024, // 10 MB default file upload size limit
- mirror: mirror,
- api_protocol: api_protocol,
- };
let app = cargo_registry::App::new(&config);
let app = cargo_registry::middleware(Arc::new(app));
@@ -147,6 +55,7 @@ fn main() {
let categories_toml = include_str!("../categories.toml");
cargo_registry::categories::sync(&categories_toml).unwrap();
+ let heroku = env::var("HEROKU").is_ok();
let port = if heroku {
8888
} else {
@@ -155,7 +64,11 @@ fn main() {
.and_then(|s| s.parse().ok())
.unwrap_or(8888)
};
- let threads = if cargo_env == Env::Development { 1 } else { 50 };
+ let threads = if config.env == Env::Development {
+ 1
+ } else {
+ 50
+ };
let mut cfg = civet::Config::new();
cfg.port(port).threads(threads).keep_alive(true);
let _a = Server::start(cfg, app);
diff --git a/src/config.rs b/src/config.rs
index 30890add680..40f91f5611b 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,5 +1,9 @@
+use s3;
+
+use std::env;
use std::path::PathBuf;
-use {Uploader, Replica};
+
+use {Env, env, Uploader, Replica};
#[derive(Clone, Debug)]
pub struct Config {
@@ -14,3 +18,115 @@ pub struct Config {
pub mirror: Replica,
pub api_protocol: String,
}
+
+impl Default for Config {
+ /// Returns a default value for the application's config
+ ///
+ /// Sets the following default values:
+ /// - `Config::max_upload_size`: 10MiB
+ /// - `Config::api_protocol`: `https`
+ ///
+ /// Pulls values from the following environment variables:
+ /// - `GIT_REPO_CHECKOUT`: The directory where the registry index was cloned.
+ /// - `MIRROR`: Is this instance of cargo_registry a mirror of crates.io.
+ /// - `HEROKU`: Is this instance of cargo_registry currently running on Heroku.
+ /// - `S3_BUCKET`: The S3 bucket used to store crate files. If not present during development,
+ /// cargo_registry will fall back to a local uploader.
+ /// - `S3_REGION`: The region in which the bucket was created. Optional if US standard.
+ /// - `S3_ACCESS_KEY`: The access key to interact with S3. Optional if running a mirror.
+ /// - `S3_SECRET_KEY`: The secret key to interact with S3. Optional if running a mirror.
+ /// - `SESSION_KEY`: The key used to sign and encrypt session cookies.
+ /// - `GH_CLIENT_ID`: The client ID of the associated GitHub application.
+ /// - `GH_CLIENT_SECRET`: The client secret of the associated GitHub application.
+ /// - `DATABASE_URL`: The URL of the postgres database to use.
+ fn default() -> Config {
+ let checkout = PathBuf::from(env("GIT_REPO_CHECKOUT"));
+ let api_protocol = String::from("https");
+ let mirror = if env::var("MIRROR").is_ok() {
+ Replica::ReadOnlyMirror
+ } else {
+ Replica::Primary
+ };
+ let heroku = env::var("HEROKU").is_ok();
+ let cargo_env = if heroku {
+ Env::Production
+ } else {
+ Env::Development
+ };
+ let uploader = match (cargo_env, mirror) {
+ (Env::Production, Replica::Primary) => {
+ // `env` panics if these vars are not set, and in production for a primary instance,
+ // that's what we want since we don't want to be able to start the server if the
+ // server doesn't know where to upload crates.
+ Uploader::S3 {
+ bucket: s3::Bucket::new(
+ env("S3_BUCKET"),
+ env::var("S3_REGION").ok(),
+ env("S3_ACCESS_KEY"),
+ env("S3_SECRET_KEY"),
+ &api_protocol,
+ ),
+ proxy: None,
+ }
+ }
+ (Env::Production, Replica::ReadOnlyMirror) => {
+ // Read-only mirrors don't need access key or secret key since by definition,
+ // they'll only need to read from a bucket, not upload.
+ //
+ // Read-only mirrors might have access key or secret key, so use them if those
+ // environment variables are set.
+ //
+ // Read-only mirrors definitely need bucket though, so that they know where
+ // to serve crate files from.
+ Uploader::S3 {
+ bucket: s3::Bucket::new(
+ env("S3_BUCKET"),
+ env::var("S3_REGION").ok(),
+ env::var("S3_ACCESS_KEY").unwrap_or_default(),
+ env::var("S3_SECRET_KEY").unwrap_or_default(),
+ &api_protocol,
+ ),
+ proxy: None,
+ }
+ }
+ // In Development mode, either running as a primary instance or a read-only mirror
+ _ => {
+ if env::var("S3_BUCKET").is_ok() {
+ // If we've set the `S3_BUCKET` variable to any value, use all of the values
+ // for the related S3 environment variables and configure the app to upload to
+ // and read from S3 like production does. All values except for bucket are
+ // optional, like production read-only mirrors.
+ println!("Using S3 uploader");
+ Uploader::S3 {
+ bucket: s3::Bucket::new(
+ env("S3_BUCKET"),
+ env::var("S3_REGION").ok(),
+ env::var("S3_ACCESS_KEY").unwrap_or_default(),
+ env::var("S3_SECRET_KEY").unwrap_or_default(),
+ &api_protocol,
+ ),
+ proxy: None,
+ }
+ } else {
+ // If we don't set the `S3_BUCKET` variable, we'll use a development-only
+ // uploader that makes it possible to run and publish to a locally-running
+ // crates.io instance without needing to set up an account and a bucket in S3.
+ println!("Using local uploader, crate files will be in the dist directory");
+ Uploader::Local
+ }
+ }
+ };
+ Config {
+ uploader: uploader,
+ session_key: env("SESSION_KEY"),
+ git_repo_checkout: checkout,
+ gh_client_id: env("GH_CLIENT_ID"),
+ gh_client_secret: env("GH_CLIENT_SECRET"),
+ db_url: env("DATABASE_URL"),
+ env: cargo_env,
+ max_upload_size: 10 * 1024 * 1024, // 10 MB default file upload size limit
+ mirror: mirror,
+ api_protocol: api_protocol,
+ }
+ }
+}
diff --git a/src/http.rs b/src/http.rs
index 552bca78913..13a7cc234c0 100644
--- a/src/http.rs
+++ b/src/http.rs
@@ -1,14 +1,21 @@
use conduit::{Request, Response};
use conduit_middleware::Middleware;
+
use curl;
use curl::easy::{Easy, List};
+
use oauth2::*;
-use app::App;
-use util::{CargoResult, internal, ChainError, human};
+
use serde_json;
use serde::Deserialize;
+
use std::str;
use std::error::Error;
+use std::collections::HashMap;
+
+use app::App;
+use util::{CargoResult, internal, ChainError, human};
+use Uploader;
/// Does all the nonsense for sending a GET to Github. Doesn't handle parsing
/// because custom error-code handling may be desirable. Use
@@ -91,8 +98,55 @@ pub fn token(token: String) -> Token {
}
}
-#[derive(Clone, Copy, Debug)]
-pub struct SecurityHeadersMiddleware;
+#[derive(Clone, Debug)]
+pub struct SecurityHeadersMiddleware {
+ headers: HashMap>,
+}
+
+impl SecurityHeadersMiddleware {
+ pub fn new(uploader: &Uploader) -> Self {
+ let mut headers = HashMap::new();
+
+ headers.insert("X-Content-Type-Options".into(), vec!["nosniff".into()]);
+
+ headers.insert("X-Frame-Options".into(), vec!["SAMEORIGIN".into()]);
+
+ headers.insert("X-XSS-Protection".into(), vec!["1; mode=block".into()]);
+
+ let s3_host = match *uploader {
+ Uploader::S3 { ref bucket, .. } => bucket.host(),
+ _ => {
+ unreachable!(
+ "This middleware should only be used in the production environment, \
+ which should also require an S3 uploader, QED"
+ )
+ }
+ };
+
+ // It would be better if we didn't have to have 'unsafe-eval' in the `script-src`
+ // policy, but google charts (used for the download graph on crate pages) uses `eval`
+ // to load scripts. Remove 'unsafe-eval' if google fixes the issue:
+ // https://github.com/google/google-visualization-issues/issues/1356
+ // or if we switch to a different graph generation library.
+ headers.insert(
+ "Content-Security-Policy".into(),
+ vec![
+ format!(
+ "default-src 'self'; \
+ connect-src 'self' https://docs.rs https://{}; \
+ script-src 'self' 'unsafe-eval' \
+ https://www.google-analytics.com https://www.google.com; \
+ style-src 'self' https://www.google.com https://ajax.googleapis.com; \
+ img-src *; \
+ object-src 'none'",
+ s3_host
+ ),
+ ],
+ );
+
+ SecurityHeadersMiddleware { headers }
+ }
+}
impl Middleware for SecurityHeadersMiddleware {
fn after(
@@ -101,36 +155,7 @@ impl Middleware for SecurityHeadersMiddleware {
mut res: Result>,
) -> Result> {
if let Ok(ref mut response) = res {
- // It would be better if we didn't have to have 'unsafe-eval' in the `script-src`
- // policy, but google charts (used for the download graph on crate pages) uses `eval`
- // to load scripts. Remove 'unsafe-eval' if google fixes the issue:
- // https://github.com/google/google-visualization-issues/issues/1356
- // or if we switch to a different graph generation library.
- response.headers.insert(
- "Content-Security-Policy".into(),
- vec![
- "default-src 'self'; \
- connect-src 'self' https://docs.rs; \
- script-src 'self' 'unsafe-eval' \
- https://www.google-analytics.com https://www.google.com; \
- style-src 'self' https://www.google.com https://ajax.googleapis.com; \
- img-src *; \
- object-src 'none'"
- .into(),
- ],
- );
- response.headers.insert(
- "X-Content-Type-Options".into(),
- vec!["nosniff".into()],
- );
- response.headers.insert(
- "X-Frame-Options".into(),
- vec!["SAMEORIGIN".into()],
- );
- response.headers.insert(
- "X-XSS-Protection".into(),
- vec!["1; mode=block".into()],
- );
+ response.headers.extend(self.headers.clone());
}
res
}
diff --git a/src/krate.rs b/src/krate.rs
index 7859b710e97..2da5fe23438 100644
--- a/src/krate.rs
+++ b/src/krate.rs
@@ -29,6 +29,7 @@ use git;
use keyword::{EncodableKeyword, CrateKeyword};
use owner::{EncodableOwner, Owner, Rights, OwnerKind, Team, rights, CrateOwner};
use pagination::Paginate;
+use render;
use schema::*;
use upload;
use user::RequestUser;
@@ -951,10 +952,22 @@ pub fn new(req: &mut Request) -> CargoResult {
let ignored_invalid_badges = Badge::update_crate(&conn, &krate, new_crate.badges.as_ref())?;
let max_version = krate.max_version(&conn)?;
+ // Render the README for this crate
+ let readme = match new_crate.readme.as_ref() {
+ Some(readme) => Some(render::markdown_to_html(&**readme)?),
+ None => None,
+ };
+
// Upload the crate, return way to delete the crate from the server
// If the git commands fail below, we shouldn't keep the crate on the
// server.
- let (cksum, mut bomb) = app.config.uploader.upload(req, &krate, max, vers)?;
+ let (cksum, mut crate_bomb, mut readme_bomb) = app.config.uploader.upload_crate(
+ req,
+ &krate,
+ readme,
+ max,
+ vers,
+ )?;
// Register this crate in our local git repo.
let git_crate = git::Crate {
@@ -973,7 +986,8 @@ pub fn new(req: &mut Request) -> CargoResult {
})?;
// Now that we've come this far, we're committed!
- bomb.path = None;
+ crate_bomb.path = None;
+ readme_bomb.path = None;
#[derive(Serialize)]
struct Warnings<'a> {
@@ -1074,6 +1088,28 @@ pub fn download(req: &mut Request) -> CargoResult {
}
}
+/// Handles the `GET /crates/:crate_id/:version/readme` route.
+pub fn readme(req: &mut Request) -> CargoResult {
+ let crate_name = &req.params()["crate_id"];
+ let version = &req.params()["version"];
+
+ let redirect_url = req.app()
+ .config
+ .uploader
+ .readme_location(crate_name, version)
+ .ok_or_else(|| human("crate readme not found"))?;
+
+ if req.wants_json() {
+ #[derive(Serialize)]
+ struct R {
+ url: String,
+ }
+ Ok(req.json(&R { url: redirect_url }))
+ } else {
+ Ok(req.redirect(redirect_url))
+ }
+}
+
fn increment_download_counts(req: &Request, crate_name: &str, version: &str) -> CargoResult<()> {
use self::versions::dsl::*;
diff --git a/src/lib.rs b/src/lib.rs
index 0f7f8bf8dac..f13cc6ae250 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -19,6 +19,7 @@ extern crate log;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;
+extern crate ammonia;
extern crate chrono;
extern crate curl;
extern crate diesel_full_text_search;
@@ -29,6 +30,7 @@ extern crate hex;
extern crate license_exprs;
extern crate oauth2;
extern crate openssl;
+extern crate pulldown_cmark;
extern crate r2d2;
extern crate r2d2_diesel;
extern crate rand;
@@ -84,6 +86,7 @@ pub mod http;
pub mod keyword;
pub mod krate;
pub mod owner;
+pub mod render;
pub mod schema;
pub mod token;
pub mod upload;
@@ -135,6 +138,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
api_router.put("/crates/new", C(krate::new));
api_router.get("/crates/:crate_id/:version", C(version::show));
api_router.get("/crates/:crate_id/:version/download", C(krate::download));
+ api_router.get("/crates/:crate_id/:version/readme", C(krate::readme));
api_router.get(
"/crates/:crate_id/:version/dependencies",
C(version::dependencies),
@@ -224,7 +228,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
env == Env::Production,
));
if env == Env::Production {
- m.add(http::SecurityHeadersMiddleware);
+ m.add(http::SecurityHeadersMiddleware::new(&app.config.uploader));
}
m.add(app::AppMiddleware::new(app));
diff --git a/src/render.rs b/src/render.rs
new file mode 100644
index 00000000000..96659be002e
--- /dev/null
+++ b/src/render.rs
@@ -0,0 +1,170 @@
+use ammonia::Ammonia;
+use pulldown_cmark::Parser;
+use pulldown_cmark::html;
+
+use util::CargoResult;
+
+/// Context for markdown to HTML rendering.
+#[allow(missing_debug_implementations)]
+pub struct MarkdownRenderer<'a> {
+ html_sanitizer: Ammonia<'a>,
+}
+
+impl<'a> MarkdownRenderer<'a> {
+ /// Creates a new renderer instance.
+ pub fn new() -> MarkdownRenderer<'a> {
+ let tags = [
+ "a",
+ "b",
+ "blockquote",
+ "br",
+ "code",
+ "dd",
+ "del",
+ "dl",
+ "dt",
+ "em",
+ "i",
+ "h1",
+ "h2",
+ "h3",
+ "hr",
+ "img",
+ "kbd",
+ "li",
+ "ol",
+ "p",
+ "pre",
+ "s",
+ "strike",
+ "strong",
+ "sub",
+ "sup",
+ "table",
+ "tbody",
+ "td",
+ "th",
+ "thead",
+ "tr",
+ "ul",
+ "hr",
+ "span",
+ ].iter()
+ .cloned()
+ .collect();
+ let tag_attributes = [
+ ("a", ["href", "target"].iter().cloned().collect()),
+ (
+ "img",
+ ["width", "height", "src", "alt", "align", "width"]
+ .iter()
+ .cloned()
+ .collect(),
+ ),
+ ("code", ["class"].iter().cloned().collect()),
+ ("span", ["style"].iter().cloned().collect()),
+ ].iter()
+ .cloned()
+ .collect();
+ let html_sanitizer = Ammonia {
+ keep_cleaned_elements: true,
+ tags: tags,
+ tag_attributes: tag_attributes,
+ ..Ammonia::default()
+ };
+ MarkdownRenderer { html_sanitizer: html_sanitizer }
+ }
+
+ /// Renders the given markdown to HTML using the current settings.
+ pub fn to_html(&self, text: &str) -> CargoResult {
+ let mut rendered = String::with_capacity(text.len() * 3 / 2);
+ let parser = Parser::new(text);
+ html::push_html(&mut rendered, parser);
+ Ok(self.html_sanitizer.clean(&rendered))
+ }
+}
+
+impl<'a> Default for MarkdownRenderer<'a> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Renders a markdown text to sanitized HTML.
+///
+/// The returned text should not contain any harmful HTML tag or attribute (such as iframe,
+/// onclick, onmouseover, etc.).
+///
+/// # Examples
+///
+/// ```
+/// use render::markdown_to_html;
+///
+/// let text = "[Rust](https://rust-lang.org/) is an awesome *systems programming* language!";
+/// let rendered = markdown_to_html(text)?;
+/// ```
+pub fn markdown_to_html(text: &str) -> CargoResult {
+ let renderer = MarkdownRenderer::new();
+ renderer.to_html(text)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn empty_text() {
+ let text = "";
+ let result = markdown_to_html(text);
+ assert_eq!(result.is_ok(), true);
+ let rendered = result.unwrap();
+ assert_eq!(rendered, "");
+ }
+
+ #[test]
+ fn text_with_script_tag() {
+ let text = "foo_readme\n\n";
+ let result = markdown_to_html(text);
+ assert_eq!(result.is_ok(), true);
+ let rendered = result.unwrap();
+ assert_eq!(rendered.contains("foo_readme"), true);
+ assert_eq!(rendered.contains("script"), false);
+ assert_eq!(rendered.contains("alert('Hello World')"), true);
+ }
+
+ #[test]
+ fn text_with_iframe_tag() {
+ let text = "foo_readme\n\n";
+ let result = markdown_to_html(text);
+ assert_eq!(result.is_ok(), true);
+ let rendered = result.unwrap();
+ assert_eq!(rendered.contains("foo_readme"), true);
+ assert_eq!(rendered.contains("iframe"), false);
+ assert_eq!(rendered.contains("alert('Hello World')"), true);
+ }
+
+ #[test]
+ fn text_with_unknwon_tag() {
+ let text = "foo_readme\n\nalert('Hello World')";
+ let result = markdown_to_html(text);
+ assert_eq!(result.is_ok(), true);
+ let rendered = result.unwrap();
+ assert_eq!(rendered.contains("foo_readme"), true);
+ assert_eq!(rendered.contains("unknown"), false);
+ assert_eq!(rendered.contains("alert('Hello World')"), true);
+ }
+
+ #[test]
+ fn text_with_inline_javascript() {
+ let text = r#"foo_readme\n\nCrate page"#;
+ let result = markdown_to_html(text);
+ assert_eq!(result.is_ok(), true);
+ let rendered = result.unwrap();
+ assert_eq!(rendered.contains("foo_readme"), true);
+ assert_eq!(rendered.contains(" Option {
match *self {
Uploader::S3 { ref bucket, .. } => {
@@ -54,35 +60,60 @@ impl Uploader {
}
}
+ /// Returns the URL of an uploaded crate's version readme.
+ ///
+ /// The function doesn't check for the existence of the file.
+ /// It returns `None` if the current `Uploader` is `NoOp`.
+ pub fn readme_location(&self, crate_name: &str, version: &str) -> Option {
+ match *self {
+ Uploader::S3 { ref bucket, .. } => {
+ Some(format!(
+ "https://{}/{}",
+ bucket.host(),
+ Uploader::readme_path(crate_name, version)
+ ))
+ }
+ Uploader::Local => {
+ Some(format!(
+ "/local_uploads/{}",
+ Uploader::readme_path(crate_name, version)
+ ))
+ }
+ Uploader::NoOp => None,
+ }
+ }
+
+ /// Returns the interna path of an uploaded crate's version archive.
fn crate_path(name: &str, version: &str) -> String {
// No slash in front so we can use join
format!("crates/{}/{}-{}.crate", name, name, version)
}
+ /// Returns the interna path of an uploaded crate's version readme.
+ fn readme_path(name: &str, version: &str) -> String {
+ format!("readmes/{}/{}-{}.html", name, name, version)
+ }
+
+ /// Uploads a file using the configured uploader (either `S3`, `Local` or `NoOp`).
+ ///
+ /// It returns a a tuple containing the path of the uploaded file
+ /// and its checksum.
pub fn upload(
&self,
- req: &mut Request,
- krate: &Crate,
- max: u64,
- vers: &semver::Version,
- ) -> CargoResult<(Vec, Bomb)> {
+ mut handle: Easy,
+ path: &str,
+ body: &mut io::Read,
+ content_type: &str,
+ content_length: u64,
+ ) -> CargoResult<(Option, Vec)> {
match *self {
Uploader::S3 { ref bucket, .. } => {
- let mut handle = req.app().handle();
- let path = format!("/{}", Uploader::crate_path(&krate.name, &vers.to_string()));
let (response, cksum) = {
- let length = read_le_u32(req.body())?;
- let body = LimitErrorReader::new(req.body(), max);
let mut body = HashingReader::new(body);
let mut response = Vec::new();
{
- let mut s3req = bucket.put(
- &mut handle,
- &path,
- &mut body,
- "application/x-tar",
- length as u64,
- );
+ let mut s3req =
+ bucket.put(&mut handle, path, &mut body, content_type, content_length);
s3req
.write_function(|data| {
response.extend(data);
@@ -102,57 +133,79 @@ impl Uploader {
response
)));
}
-
- Ok((
- cksum,
- Bomb {
- app: req.app().clone(),
- path: Some(path),
- },
- ))
+ Ok((Some(String::from(path)), cksum))
}
Uploader::Local => {
- let path = Uploader::crate_path(&krate.name, &vers.to_string());
- let crate_filename = env::current_dir()
+ let filename = env::current_dir()
.unwrap()
.join("dist")
.join("local_uploads")
.join(path);
-
- let crate_dir = crate_filename.parent().unwrap();
- fs::create_dir_all(crate_dir)?;
-
- let mut crate_file = File::create(&crate_filename)?;
-
- let cksum = {
- read_le_u32(req.body())?;
- let body = LimitErrorReader::new(req.body(), max);
- let mut body = HashingReader::new(body);
-
- io::copy(&mut body, &mut crate_file)?;
- body.finalize()
- };
-
- Ok((
- cksum,
- Bomb {
- app: req.app().clone(),
- path: crate_filename.to_str().map(String::from),
- },
- ))
- }
- Uploader::NoOp => {
- Ok((
- vec![],
- Bomb {
- app: req.app().clone(),
- path: None,
- },
- ))
+ let dir = filename.parent().unwrap();
+ fs::create_dir_all(dir)?;
+ let mut file = File::create(&filename)?;
+ let mut body = HashingReader::new(body);
+ io::copy(&mut body, &mut file)?;
+ Ok((filename.to_str().map(String::from), body.finalize()))
}
+ Uploader::NoOp => Ok((None, vec![])),
}
}
+ /// Uploads a crate and its readme. Returns the checksum of the uploaded crate
+ /// file, and bombs for the uploaded crate and the uploaded readme.
+ pub fn upload_crate(
+ &self,
+ req: &mut Request,
+ krate: &Crate,
+ readme: Option,
+ max: u64,
+ vers: &semver::Version,
+ ) -> CargoResult<(Vec, Bomb, Bomb)> {
+ let app = req.app().clone();
+ let (crate_path, checksum) = {
+ let path = Uploader::crate_path(&krate.name, &vers.to_string());
+ let length = read_le_u32(req.body())?;
+ let mut body = LimitErrorReader::new(req.body(), max);
+ self.upload(
+ app.handle(),
+ &path,
+ &mut body,
+ "application/x-tar",
+ length as u64,
+ )?
+ };
+ // We create the bomb for the crate file before uploading the readme so that if the
+ // readme upload fails, the uploaded crate file is automatically deleted.
+ let crate_bomb = Bomb {
+ app: app.clone(),
+ path: crate_path,
+ };
+ let (readme_path, _) = if let Some(rendered) = readme {
+ let path = Uploader::readme_path(&krate.name, &vers.to_string());
+ let length = rendered.len();
+ let mut body = io::Cursor::new(rendered.into_bytes());
+ self.upload(
+ app.handle(),
+ &path,
+ &mut body,
+ "text/html",
+ length as u64,
+ )?
+ } else {
+ (None, vec![])
+ };
+ Ok((
+ checksum,
+ crate_bomb,
+ Bomb {
+ app: app.clone(),
+ path: readme_path,
+ },
+ ))
+ }
+
+ /// Deletes an uploaded file.
fn delete(&self, app: Arc, path: &str) -> CargoResult<()> {
match *self {
Uploader::S3 { ref bucket, .. } => {
diff --git a/src/version.rs b/src/version.rs
index e45f1209935..1461d2df6d4 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -53,6 +53,7 @@ pub struct EncodableVersion {
pub krate: String,
pub num: String,
pub dl_path: String,
+ pub readme_path: String,
pub updated_at: String,
pub created_at: String,
pub downloads: i32,
@@ -85,6 +86,7 @@ impl Version {
let num = num.to_string();
EncodableVersion {
dl_path: format!("/api/v1/crates/{}/{}/download", crate_name, num),
+ readme_path: format!("/api/v1/crates/{}/{}/readme", crate_name, num),
num: num.clone(),
id: id,
krate: crate_name.to_string(),