about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMingwei Samuel <mingwes@amazon.com>2025-07-25 13:21:50 -0700
committerMingwei Samuel <mingwes@amazon.com>2025-08-01 14:42:17 -0700
commitf516d4c9e341847d8f452adef68fe99cfdcfccc0 (patch)
tree931f4fe5de66d4eeb2eeb903368d08ab9b35d07b
parent4b55fe199cfe9c710555a5af7f2a49491ad38254 (diff)
downloadrust-f516d4c9e341847d8f452adef68fe99cfdcfccc0.tar.gz
rust-f516d4c9e341847d8f452adef68fe99cfdcfccc0.zip
rustdoc font links only emit `crossorigin` when needed
The `crossorigin` attribute may cause issues when the href is not actuall across origins. Specifically, the tag causes the browser to send a preflight OPTIONS request to the href even if it is same-origin.
Some tempermental servers may reject all CORS preflect requests even if they're actually same-origin, which causes a CORS error and prevents the fonts from loading, even later on.

This commit fixes that problem by not emitting `crossorigin` if the url looks like a domain-relative url.

Co-authored-by: Guillaume Gomez <guillaume1.gomez@gmail.com>
-rw-r--r--src/librustdoc/html/layout.rs29
-rw-r--r--src/librustdoc/html/layout/tests.rs24
-rw-r--r--src/librustdoc/html/templates/page.html2
3 files changed, 54 insertions, 1 deletions
diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs
index 1f92c521d46..2782e8e0058 100644
--- a/src/librustdoc/html/layout.rs
+++ b/src/librustdoc/html/layout.rs
@@ -8,6 +8,9 @@ use super::static_files::{STATIC_FILES, StaticFiles};
 use crate::externalfiles::ExternalHtml;
 use crate::html::render::{StylePath, ensure_trailing_slash};
 
+#[cfg(test)]
+mod tests;
+
 pub(crate) struct Layout {
     pub(crate) logo: String,
     pub(crate) favicon: String,
@@ -68,6 +71,13 @@ struct PageLayout<'a> {
     display_krate_version_extra: &'a str,
 }
 
+impl PageLayout<'_> {
+    /// See [`may_remove_crossorigin`].
+    fn static_root_path_may_remove_crossorigin(&self) -> bool {
+        may_remove_crossorigin(&self.static_root_path)
+    }
+}
+
 pub(crate) use crate::html::render::sidebar::filters;
 
 pub(crate) fn render<T: Display, S: Display>(
@@ -134,3 +144,22 @@ pub(crate) fn redirect(url: &str) -> String {
 </html>"##,
     )
 }
+
+/// Conservatively determines if `href` is relative to the current origin,
+/// so that `crossorigin` may be safely removed from `<link>` elements.
+pub(crate) fn may_remove_crossorigin(href: &str) -> bool {
+    // Reject scheme-relative URLs (`//example.com/`).
+    if href.starts_with("//") {
+        return false;
+    }
+    // URL is interpreted as having a scheme iff: it starts with an ascii alpha, and only
+    // contains ascii alphanumeric or `+` `-` `.` up to the `:`.
+    // https://url.spec.whatwg.org/#url-parsing
+    let has_scheme = href.split_once(':').is_some_and(|(scheme, _rest)| {
+        let mut chars = scheme.chars();
+        chars.next().is_some_and(|c| c.is_ascii_alphabetic())
+            && chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
+    });
+    // Reject anything with a scheme (`http:`, etc.).
+    !has_scheme
+}
diff --git a/src/librustdoc/html/layout/tests.rs b/src/librustdoc/html/layout/tests.rs
new file mode 100644
index 00000000000..d4a19ee9abf
--- /dev/null
+++ b/src/librustdoc/html/layout/tests.rs
@@ -0,0 +1,24 @@
+#[test]
+fn test_may_remove_crossorigin() {
+    use super::may_remove_crossorigin;
+
+    assert!(may_remove_crossorigin("font.woff2"));
+    assert!(may_remove_crossorigin("/font.woff2"));
+    assert!(may_remove_crossorigin("./font.woff2"));
+    assert!(may_remove_crossorigin(":D/font.woff2"));
+    assert!(may_remove_crossorigin("../font.woff2"));
+
+    assert!(!may_remove_crossorigin("//example.com/static.files"));
+    assert!(!may_remove_crossorigin("http://example.com/static.files"));
+    assert!(!may_remove_crossorigin("https://example.com/static.files"));
+    assert!(!may_remove_crossorigin("https://example.com:8080/static.files"));
+
+    assert!(!may_remove_crossorigin("ftp://example.com/static.files"));
+    assert!(!may_remove_crossorigin("blob:http://example.com/static.files"));
+    assert!(!may_remove_crossorigin("javascript:alert('Hello, world!')"));
+    assert!(!may_remove_crossorigin("//./C:"));
+    assert!(!may_remove_crossorigin("file:////C:"));
+    assert!(!may_remove_crossorigin("file:///./C:"));
+    assert!(!may_remove_crossorigin("data:,Hello%2C%20World%21"));
+    assert!(!may_remove_crossorigin("hi...:hello"));
+}
diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html
index 7af99e7097c..398436e3fe1 100644
--- a/src/librustdoc/html/templates/page.html
+++ b/src/librustdoc/html/templates/page.html
@@ -7,7 +7,7 @@
     <meta name="description" content="{{page.description}}"> {# #}
     <title>{{page.title}}</title> {# #}
     <script>if(window.location.protocol!=="file:") {# Hack to skip preloading fonts locally - see #98769 #}
-    document.head.insertAdjacentHTML("beforeend","{{files.source_serif_4_regular}},{{files.fira_sans_italic}},{{files.fira_sans_regular}},{{files.fira_sans_medium_italic}},{{files.fira_sans_medium}},{{files.source_code_pro_regular}},{{files.source_code_pro_semibold}}".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2" crossorigin href="{{static_root_path|safe}}${f}">`).join("")) {# #}
+    document.head.insertAdjacentHTML("beforeend","{{files.source_serif_4_regular}},{{files.fira_sans_italic}},{{files.fira_sans_regular}},{{files.fira_sans_medium_italic}},{{files.fira_sans_medium}},{{files.source_code_pro_regular}},{{files.source_code_pro_semibold}}".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"{% if !static_root_path_may_remove_crossorigin() %} crossorigin{% endif %} href="{{static_root_path|safe}}${f}">`).join("")) {# #}
     </script> {# #}
     <link rel="stylesheet" {#+ #}
           href="{{static_root_path|safe}}{{files.normalize_css}}"> {# #}