-
Notifications
You must be signed in to change notification settings - Fork 211
/
Copy pathrustdoc.rs
405 lines (352 loc) · 15 KB
/
rustdoc.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! rustdoc handler
use super::pool::Pool;
use super::file::File;
use super::{latest_version, redirect_base};
use super::crate_details::CrateDetails;
use iron::prelude::*;
use iron::{status, Url};
use iron::modifiers::Redirect;
use router::Router;
use super::{match_version, MatchVersion};
use super::error::Nope;
use super::page::Page;
use rustc_serialize::json::{Json, ToJson};
use std::collections::BTreeMap;
use iron::headers::{Expires, HttpDate, CacheControl, CacheDirective};
use postgres::Connection;
use time;
use iron::Handler;
use utils;
#[derive(Debug)]
struct RustdocPage {
pub head: String,
pub body: String,
pub body_class: String,
pub name: String,
pub full: String,
pub version: String,
pub description: Option<String>,
pub crate_details: Option<CrateDetails>,
}
impl Default for RustdocPage {
fn default() -> RustdocPage {
RustdocPage {
head: String::new(),
body: String::new(),
body_class: String::new(),
name: String::new(),
full: String::new(),
version: String::new(),
description: None,
crate_details: None,
}
}
}
impl ToJson for RustdocPage {
fn to_json(&self) -> Json {
let mut m: BTreeMap<String, Json> = BTreeMap::new();
m.insert("rustdoc_head".to_string(), self.head.to_json());
m.insert("rustdoc_body".to_string(), self.body.to_json());
m.insert("rustdoc_body_class".to_string(), self.body_class.to_json());
m.insert("rustdoc_full".to_string(), self.full.to_json());
m.insert("rustdoc_status".to_string(), true.to_json());
m.insert("name".to_string(), self.name.to_json());
m.insert("version".to_string(), self.version.to_json());
m.insert("description".to_string(), self.description.to_json());
m.insert("crate_details".to_string(), self.crate_details.to_json());
m.to_json()
}
}
pub struct RustLangRedirector {
url: Url,
}
impl RustLangRedirector {
pub fn new(target: &'static str) -> Self {
let url = url::Url::parse("https://doc.rust-lang.org/stable/")
.expect("failed to parse rust-lang.org base URL")
.join(target)
.expect("failed to append crate name to rust-lang.org base URL");
let url = Url::from_generic_url(url)
.expect("failed to convert url::Url to iron::Url");
Self { url }
}
}
impl iron::Handler for RustLangRedirector {
fn handle(&self, _req: &mut Request) -> IronResult<Response> {
Ok(Response::with((status::Found, Redirect(self.url.clone()))))
}
}
/// Handler called for `/:crate` and `/:crate/:version` URLs. Automatically redirects to the docs
/// or crate details page based on whether the given crate version was successfully built.
pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult<Response> {
fn redirect_to_doc(req: &Request,
name: &str,
vers: &str,
target_name: &str)
-> IronResult<Response> {
let url = ctry!(Url::parse(&format!("{}/{}/{}/{}/",
redirect_base(req),
name,
vers,
target_name)[..]));
let mut resp = Response::with((status::Found, Redirect(url)));
resp.headers.set(Expires(HttpDate(time::now())));
Ok(resp)
}
fn redirect_to_crate(req: &Request,
name: &str,
vers: &str)
-> IronResult<Response> {
let url = ctry!(Url::parse(&format!("{}/crate/{}/{}",
redirect_base(req),
name,
vers)[..]));
let mut resp = Response::with((status::Found, Redirect(url)));
resp.headers.set(Expires(HttpDate(time::now())));
Ok(resp)
}
// this unwrap is safe because iron urls are always able to use `path_segments`
// i'm using this instead of `req.url.path()` to avoid allocating the Vec, and also to avoid
// keeping the borrow alive into the return statement
if req.url.as_ref().path_segments().unwrap().last().map_or(false, |s| s.ends_with(".js")) {
// javascript files should be handled by the file server instead of erroneously
// redirecting to the crate root page
if req.url.as_ref().path_segments().unwrap().count() > 2 {
// this URL is actually from a crate-internal path, serve it there instead
return rustdoc_html_server_handler(req);
} else {
let path = req.url.path();
let path = path.join("/");
let conn = extension!(req, Pool);
match File::from_path(&conn, &path) {
Some(f) => return Ok(f.serve()),
None => return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)),
}
}
} else if req.url.as_ref().path_segments().unwrap().last().map_or(false, |s| s.ends_with(".ico")) {
// route .ico files into their dedicated handler so that docs.rs's favicon is always
// displayed
return super::ico_handler(req);
}
let router = extension!(req, Router);
// this handler should never called without crate pattern
let crate_name = cexpect!(router.find("crate"));
let req_version = router.find("version");
let conn = extension!(req, Pool);
// it doesn't matter if the version that was given was exact or not, since we're redirecting
// anyway
let version = match match_version(&conn, &crate_name, req_version).into_option() {
Some(v) => v,
None => return Err(IronError::new(Nope::CrateNotFound, status::NotFound)),
};
// get target name and whether it has docs
// FIXME: This is a bit inefficient but allowing us to use less code in general
let (target_name, has_docs): (String, bool) = {
let rows = ctry!(conn.query("SELECT target_name, rustdoc_status
FROM releases
INNER JOIN crates ON crates.id = releases.crate_id
WHERE crates.name = $1 AND releases.version = $2",
&[&crate_name, &version]));
(rows.get(0).get(0), rows.get(0).get(1))
};
if has_docs {
redirect_to_doc(req, &crate_name, &version, &target_name)
} else {
redirect_to_crate(req, &crate_name, &version)
}
}
/// Serves documentation generated by rustdoc.
///
/// This includes all HTML files for an individual crate, as well as the `search-index.js`, which is
/// also crate-specific.
pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult<Response> {
let router = extension!(req, Router);
let name = router.find("crate").unwrap_or("").to_string();
let url_version = router.find("version");
let version; // pre-declaring it to enforce drop order relative to `req_path`
let conn = extension!(req, Pool);
let mut req_path = req.url.path();
// remove name and version from path
for _ in 0..2 {
req_path.remove(0);
}
version = match match_version(&conn, &name, url_version) {
MatchVersion::Exact(v) => v,
MatchVersion::Semver(v) => {
// to prevent cloudfront caching the wrong artifacts on URLs with loose semver
// versions, redirect the browser to the returned version instead of loading it
// immediately
let url = ctry!(Url::parse(&format!("{}/{}/{}/{}",
redirect_base(req),
name,
v,
req_path.join("/"))[..]));
return Ok(super::redirect(url));
}
MatchVersion::None => return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)),
};
// docs have "rustdoc" prefix in database
req_path.insert(0, "rustdoc");
// add crate name and version
req_path.insert(1, &name);
req_path.insert(2, &version);
let path = {
let mut path = req_path.join("/");
if path.ends_with('/') {
req_path.pop(); // get rid of empty string
path.push_str("index.html");
req_path.push("index.html");
}
path
};
let file = match File::from_path(&conn, &path) {
Some(f) => f,
None => return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)),
};
// serve file directly if it's not html
if !path.ends_with(".html") {
return Ok(file.serve());
}
let mut content = RustdocPage::default();
let file_content = ctry!(String::from_utf8(file.0.content));
let (head, body, mut body_class) = ctry!(utils::extract_head_and_body(&file_content));
content.head = head;
content.body = body;
if body_class.is_empty() {
body_class = "rustdoc container-rustdoc".to_string();
} else {
// rustdoc adds its own "rustdoc" class to the body
body_class.push_str(" container-rustdoc");
}
content.body_class = body_class;
content.full = file_content;
let crate_details = cexpect!(CrateDetails::new(&conn, &name, &version));
let (path, version) = if let Some(version) = latest_version(&crate_details.versions, &version) {
req_path[2] = &version;
(path_for_version(&req_path, &crate_details.target_name, &conn), version)
} else {
Default::default()
};
content.crate_details = Some(crate_details);
Page::new(content)
.set_true("show_package_navigation")
.set_true("package_navigation_documentation_tab")
.set_true("package_navigation_show_platforms_tab")
.set_bool("is_latest_version", path.is_empty())
.set("path_in_latest", &path)
.set("latest_version", &version)
.to_resp("rustdoc")
}
/// Checks whether the given path exists.
/// The crate's `target_name` is used to confirm whether a platform triple is part of the path.
///
/// Note that path is overloaded in this context to mean both the path of a URL
/// and the file path of a static file in the DB.
///
/// `req_path` is assumed to have the following format:
/// `rustdoc/crate/version[/platform]/module/[kind.name.html|index.html]`
///
/// Returns a path that can be appended to `/crate/version/` to create a complete URL.
fn path_for_version(req_path: &[&str], target_name: &str, conn: &Connection) -> String {
// Simple case: page exists in the latest version, so just change the version number
if File::from_path(&conn, &req_path.join("/")).is_some() {
// NOTE: this adds 'index.html' if it wasn't there before
return req_path[3..].join("/");
}
// this page doesn't exist in the latest version
let search_item = if *req_path.last().unwrap() == "index.html" {
// this is a module
req_path[req_path.len() - 2]
} else {
// this is an item
req_path.last().unwrap().split('.').nth(1)
.expect("paths should be of the form <kind>.<name>.html")
};
// check if req_path[3] is the platform choice or the name of the crate
let concat_path;
let crate_root = if req_path[3] != target_name {
concat_path = format!("{}/{}", req_path[3], req_path[4]);
&concat_path
} else {
req_path[3]
};
format!("{}/?search={}", crate_root, search_item)
}
pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
use iron::headers::ContentType;
use params::{Params, Value};
use badge::{Badge, BadgeOptions};
let version = {
let params = ctry!(req.get_ref::<Params>());
match params.find(&["version"]) {
Some(&Value::String(ref version)) => version.clone(),
_ => "*".to_owned(),
}
};
let name = cexpect!(extension!(req, Router).find("crate"));
let conn = extension!(req, Pool);
let options = match match_version(&conn, &name, Some(&version)) {
MatchVersion::Exact(version) => {
let rows = ctry!(conn.query("SELECT rustdoc_status
FROM releases
INNER JOIN crates ON crates.id = releases.crate_id
WHERE crates.name = $1 AND releases.version = $2",
&[&name, &version]));
if rows.len() > 0 && rows.get(0).get(0) {
BadgeOptions {
subject: "docs".to_owned(),
status: version,
color: "#4d76ae".to_owned(),
}
} else {
BadgeOptions {
subject: "docs".to_owned(),
status: version,
color: "#e05d44".to_owned(),
}
}
}
MatchVersion::Semver(version) => {
let url = ctry!(Url::parse(&format!("{}/{}/badge.svg?version={}",
redirect_base(req),
name,
version)[..]));
return Ok(super::redirect(url));
}
MatchVersion::None => {
BadgeOptions {
subject: "docs".to_owned(),
status: "no builds".to_owned(),
color: "#e05d44".to_owned(),
}
}
};
let mut resp = Response::with((status::Ok, ctry!(Badge::new(options)).to_svg()));
resp.headers.set(ContentType("image/svg+xml".parse().unwrap()));
resp.headers.set(Expires(HttpDate(time::now())));
resp.headers.set(CacheControl(vec![CacheDirective::NoCache,
CacheDirective::NoStore,
CacheDirective::MustRevalidate]));
Ok(resp)
}
/// Serves shared web resources used by rustdoc-generated documentation.
///
/// This includes common `css` and `js` files that only change when the compiler is updated, but are
/// otherwise the same for all crates documented with that compiler. Those have a custom handler to
/// deduplicate them and save space.
pub struct SharedResourceHandler;
impl Handler for SharedResourceHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
let path = req.url.path();
let filename = path.last().unwrap(); // unwrap is fine: vector is non-empty
let suffix = filename.split('.').last().unwrap(); // unwrap is fine: split always works
if ["js", "css", "woff", "svg"].contains(&suffix) {
let conn = extension!(req, Pool);
if let Some(file) = File::from_path(conn, filename) {
return Ok(file.serve());
}
}
// Just always return a 404 here - the main handler will then try the other handlers
Err(IronError::new(Nope::ResourceNotFound, status::NotFound))
}
}