Skip to content

Commit 1704601

Browse files
committed
support /latest/ and others in build-details handler for consistency
1 parent 7dfb7f4 commit 1704601

File tree

5 files changed

+165
-41
lines changed

5 files changed

+165
-41
lines changed

.sqlx/query-61e8789f7d2ddcfa951dc86c5f455cf4451f8f3b683c6bd1033e8ebaab97692e.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Justfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ sqlx-prepare ADDITIONAL_ARGS="":
1111
sqlx-check:
1212
just sqlx-prepare "--check"
1313

14-
lint:
15-
cargo clippy --all-features --all-targets --workspace --locked -- -D warnings
14+
lint *args:
15+
cargo clippy --all-features --all-targets --workspace --locked {{ args }} -- -D warnings
16+
17+
lint-fix:
18+
just lint --fix --allow-dirty --allow-staged
1619

1720
lint-js *args:
1821
deno run -A npm:eslint@9 static templates gui-tests eslint.config.js {{ args }}

src/web/build_details.rs

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
use crate::{
22
AsyncStorage, Config,
3-
db::{
4-
BuildId,
5-
types::{BuildStatus, version::Version},
6-
},
3+
db::{BuildId, types::BuildStatus},
74
impl_axum_webpage,
85
web::{
96
MetaData,
7+
cache::CachePolicy,
108
error::{AxumNope, AxumResult},
119
extractors::{DbConnection, Path, rustdoc::RustdocParams},
1210
file::File,
13-
filters,
11+
filters, match_version,
1412
page::templates::{RenderBrands, RenderRegular, RenderSolid},
1513
},
1614
};
@@ -55,24 +53,37 @@ impl BuildDetailsPage {
5553

5654
#[derive(Clone, Deserialize, Debug)]
5755
pub(crate) struct BuildDetailsParams {
58-
pub(crate) name: String,
59-
pub(crate) version: Version,
6056
pub(crate) id: String,
6157
pub(crate) filename: Option<String>,
6258
}
6359

6460
pub(crate) async fn build_details_handler(
65-
Path(params): Path<BuildDetailsParams>,
61+
params: RustdocParams,
62+
Path(build_params): Path<BuildDetailsParams>,
6663
mut conn: DbConnection,
6764
Extension(config): Extension<Arc<Config>>,
6865
Extension(storage): Extension<Arc<AsyncStorage>>,
6966
) -> AxumResult<impl IntoResponse> {
70-
let id = params
67+
let id = build_params
7168
.id
7269
.parse()
7370
.map(BuildId)
7471
.map_err(|_| AxumNope::BuildNotFound)?;
7572

73+
let version = match_version(&mut conn, params.name(), params.req_version())
74+
.await?
75+
.assume_exact_name()?
76+
.into_canonical_req_version_or_else(|version| {
77+
AxumNope::Redirect(
78+
params
79+
.clone()
80+
.with_req_version(version)
81+
.build_details_url(id, build_params.filename.as_deref()),
82+
CachePolicy::ForeverInCdn,
83+
)
84+
})?
85+
.into_version();
86+
7687
let row = sqlx::query!(
7788
r#"SELECT
7889
builds.rustc_version,
@@ -87,8 +98,8 @@ pub(crate) async fn build_details_handler(
8798
INNER JOIN crates ON releases.crate_id = crates.id
8899
WHERE builds.id = $1 AND crates.name = $2 AND releases.version = $3"#,
89100
id.0,
90-
params.name,
91-
params.version.to_string(),
101+
params.name(),
102+
version as _
92103
)
93104
.fetch_optional(&mut *conn)
94105
.await?
@@ -114,7 +125,7 @@ pub(crate) async fn build_details_handler(
114125
.try_collect()
115126
.await?;
116127

117-
let current_filename = if let Some(filename) = params.filename {
128+
let current_filename = if let Some(filename) = build_params.filename {
118129
// if we have a given filename in the URL, we use that one.
119130
Some(filename)
120131
} else if let Some(default_target) = row.default_target {
@@ -144,8 +155,14 @@ pub(crate) async fn build_details_handler(
144155
(file_content, all_log_filenames, current_filename)
145156
};
146157

147-
let metadata = MetaData::from_crate(&mut conn, &params.name, &params.version, None).await?;
148-
let params = RustdocParams::from_metadata(&metadata);
158+
let metadata = MetaData::from_crate(
159+
&mut conn,
160+
params.name(),
161+
&version,
162+
Some(params.req_version().clone()),
163+
)
164+
.await?;
165+
let params = params.apply_metadata(&metadata);
149166

150167
Ok(BuildDetailsPage {
151168
metadata,
@@ -167,9 +184,12 @@ pub(crate) async fn build_details_handler(
167184

168185
#[cfg(test)]
169186
mod tests {
170-
use crate::test::{
171-
AxumResponseTestExt, AxumRouterTestExt, FakeBuild, async_wrapper,
172-
fake_release_that_failed_before_build,
187+
use crate::{
188+
db::types::{BuildId, ReleaseId},
189+
test::{
190+
AxumResponseTestExt, AxumRouterTestExt, FakeBuild, TestEnvironment, V0_1,
191+
async_wrapper, fake_release_that_failed_before_build,
192+
},
173193
};
174194
use kuchikiki::traits::TendrilSink;
175195
use test_case::test_case;
@@ -187,6 +207,22 @@ mod tests {
187207
.collect()
188208
}
189209

210+
async fn build_ids_for_release(
211+
conn: &mut sqlx::PgConnection,
212+
release_id: ReleaseId,
213+
) -> Vec<BuildId> {
214+
sqlx::query!(
215+
"SELECT id FROM builds WHERE rid = $1 ORDER BY id ASC",
216+
release_id as _
217+
)
218+
.fetch_all(conn)
219+
.await
220+
.unwrap()
221+
.into_iter()
222+
.map(|row| BuildId(row.id))
223+
.collect()
224+
}
225+
190226
#[test]
191227
fn test_partial_build_result() {
192228
async_wrapper(|env| async move {
@@ -475,4 +511,30 @@ mod tests {
475511
Ok(())
476512
});
477513
}
514+
515+
#[tokio::test(flavor = "multi_thread")]
516+
async fn build_detail_via_latest() -> anyhow::Result<()> {
517+
let env = TestEnvironment::new().await?;
518+
let rid = env
519+
.fake_release()
520+
.await
521+
.name("foo")
522+
.version(V0_1)
523+
.create()
524+
.await?;
525+
526+
let mut conn = env.async_db().async_conn().await;
527+
let build_id = {
528+
let ids = build_ids_for_release(&mut conn, rid).await;
529+
assert_eq!(ids.len(), 1);
530+
ids[0]
531+
};
532+
533+
env.web_app()
534+
.await
535+
.assert_success(&format!("/crate/foo/latest/builds/{build_id}"))
536+
.await?;
537+
538+
Ok(())
539+
}
478540
}

src/web/extractors/rustdoc.rs

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ use std::borrow::Cow;
2020
const INDEX_HTML: &str = "index.html";
2121
const FOLDER_AND_INDEX_HTML: &str = "/index.html";
2222

23+
pub(crate) const ROOT_RUSTDOC_HTML_FILES: &[&str] = &[
24+
"all.html",
25+
"help.html",
26+
"settings.html",
27+
"scrape-examples-help.html",
28+
];
29+
2330
#[derive(Clone, Debug, PartialEq)]
2431
pub(crate) enum PageKind {
2532
Rustdoc,
@@ -723,27 +730,38 @@ fn generate_rustdoc_path_for_url(
723730
&& !path.is_empty()
724731
&& path != INDEX_HTML
725732
{
726-
if let Some(target_name) = target_name
727-
&& path == target_name
728-
{
729-
// Sometimes people add a path that is just the target name.
730-
// This matches our `rustdoc_redirector_handler` route,
731-
// without trailing slash (`/{name}/{version}/{target}`).
732-
//
733-
// Even though the rustdoc html handler would also be capable
734-
// handling this input, we want to redirect to the trailing
735-
// slash version (`/{name}/{version}/{target}/`),
736-
// so they are separate.
737-
format!("{}/", target_name)
738-
} else {
733+
// for none-elements paths we have to guarantee that we have a
734+
// trailing slash, otherwise the rustdoc-url won't hit the html-handler and
735+
// lead to redirect loops.
736+
if path.contains('/') {
739737
// just use the given inner to start, if:
740738
// * it's not empty
741739
// * it's not just "index.html"
740+
// * we have a slash in the path.
742741
path.to_string()
742+
} else if ROOT_RUSTDOC_HTML_FILES.iter().any(|&f| f == path) {
743+
// special case: some files are at the root of the rustdoc output,
744+
// without a trailing slash, and the routes are fine with that.
745+
// e.g. `/help.html`, `/settings.html`, `/all.html`, ...
746+
path.to_string()
747+
} else if let Some(target_name) = target_name {
748+
if target_name == path {
749+
// when we have the target name as path, without a trailing slash,
750+
// just add the slash.
751+
format!("{}/", path)
752+
} else {
753+
// when someone just attaches some path to the URL, like
754+
// `/{krate}/{version}/somefile.html`, we assume they meant
755+
// `/{krate}/{version}/{target_name}/somefile.html`.
756+
format!("{}/{}", target_name, path)
757+
}
758+
} else {
759+
// fallback: just attach a slash and redirect.
760+
format!("{}/", path)
743761
}
744762
} else if let Some(target_name) = target_name {
745763
// after having no usable given path, we generate one with the
746-
// target name, if we have one.
764+
// target name, if we have one/.
747765
format!("{}/", target_name)
748766
} else {
749767
// no usable given path:
@@ -1639,23 +1657,26 @@ mod tests {
16391657
assert_eq!(params.rustdoc_url(), "/krate/latest/krate/");
16401658
}
16411659

1642-
#[test_case(Some(PageKind::Rustdoc))]
1643-
#[test_case(None)]
1644-
fn test_only_target_name_without_slash(page_kind: Option<PageKind>) {
1660+
#[test_case("other_path.html", "/krate/latest/krate/other_path.html")]
1661+
#[test_case("other_path", "/krate/latest/krate/other_path"; "without .html")]
1662+
#[test_case("other_path.html", "/krate/latest/krate/other_path.html"; "with .html")]
1663+
#[test_case("settings.html", "/krate/latest/settings.html"; "static routes")]
1664+
#[test_case(KRATE, "/krate/latest/krate/"; "same as target name, without slash")]
1665+
fn test_redirect_some_odd_paths_we_saw(inner_path: &str, expected_url: &str) {
16451666
// test for https://github.com/rust-lang/docs.rs/issues/2989
16461667
let params = RustdocParams::new(KRATE)
1647-
.with_maybe_page_kind(page_kind)
1648-
.try_with_original_uri("/dummy/latest/dummy")
1668+
.with_page_kind(PageKind::Rustdoc)
1669+
.try_with_original_uri(format!("/{KRATE}/latest/{inner_path}"))
16491670
.unwrap()
16501671
.with_req_version(ReqVersion::Latest)
16511672
.with_maybe_doc_target(None::<String>)
1652-
.with_inner_path(KRATE)
1673+
.with_inner_path(inner_path)
16531674
.with_default_target(DEFAULT_TARGET)
16541675
.with_target_name(KRATE)
16551676
.with_doc_targets(TARGETS.iter().cloned());
16561677

16571678
dbg!(&params);
16581679

1659-
assert_eq!(params.rustdoc_url(), "/krate/latest/krate/");
1680+
assert_eq!(params.rustdoc_url(), expected_url);
16601681
}
16611682
}

src/web/rustdoc.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ pub(crate) async fn rustdoc_redirector_handler(
219219
let params = params.with_page_kind(PageKind::Rustdoc);
220220

221221
fn redirect_to_doc(
222+
original_uri: Option<&Uri>,
222223
url: EscapedURI,
223224
cache_policy: CachePolicy,
224225
path_in_crate: Option<&str>,
@@ -229,6 +230,17 @@ pub(crate) async fn rustdoc_redirector_handler(
229230
url
230231
};
231232

233+
if let Some(original_uri) = original_uri
234+
&& original_uri.path() == original_uri.path()
235+
{
236+
return Err(anyhow!(
237+
"infinite redirect detected, \noriginal_uri = {}, redirect_url = {}",
238+
original_uri,
239+
url
240+
)
241+
.into());
242+
}
243+
232244
trace!(%url, ?cache_policy, path_in_crate, "redirect to doc");
233245
Ok(axum_cached_redirect(url, cache_policy)?)
234246
}
@@ -266,6 +278,7 @@ pub(crate) async fn rustdoc_redirector_handler(
266278
let target_uri =
267279
EscapedURI::from_uri(description.href.clone()).append_raw_query(original_query);
268280
return redirect_to_doc(
281+
params.original_uri(),
269282
target_uri,
270283
CachePolicy::ForeverInCdnAndStaleInBrowser,
271284
path_in_crate.as_deref(),
@@ -329,6 +342,7 @@ pub(crate) async fn rustdoc_redirector_handler(
329342

330343
if matched_release.rustdoc_status() {
331344
Ok(redirect_to_doc(
345+
params.original_uri(),
332346
params.rustdoc_url().append_raw_query(original_query),
333347
if matched_release.is_latest_url() {
334348
CachePolicy::ForeverInCdn
@@ -3345,7 +3359,9 @@ mod test {
33453359
#[tokio::test(flavor = "multi_thread")]
33463360
#[test_case("/dummy/"; "only krate")]
33473361
#[test_case("/dummy/latest/"; "with version")]
3348-
#[test_case("/dummy/latest/dummy"; "full without trailing slash")]
3362+
#[test_case("/dummy/latest/dummy"; "target-name as path, without trailing slash")]
3363+
#[test_case("/dummy/latest/other_path"; "other path, without trailing slash")]
3364+
#[test_case("/dummy/latest/other_path.html"; "other html path, without trailing slash")]
33493365
#[test_case("/dummy/latest/dummy/"; "final target")]
33503366
async fn test_full_latest_url_without_trailing_slash(path: &str) -> Result<()> {
33513367
// test for https://github.com/rust-lang/docs.rs/issues/2989

0 commit comments

Comments
 (0)