a73x

426024af

fix(server): cap plain-text readme render and document url assumption

alex emery   2026-04-13 05:43


diff --git a/src/server/http/repo/readme.rs b/src/server/http/repo/readme.rs
index 6672521..a7bab9e 100644
--- a/src/server/http/repo/readme.rs
+++ b/src/server/http/repo/readme.rs
@@ -57,11 +57,24 @@ pub fn load_readme(
                rendered
            }
        }
        ReadmeKind::Plain => render_plain(text),
        ReadmeKind::Plain => {
            let rendered = render_plain(text);
            if rendered.len() > MAX_HTML_BYTES {
                too_large_notice_with_repo(repo_name, branch, &entry.name)
            } else {
                rendered
            }
        }
    };
    Some(RenderedReadme { html })
}

/// Build the "README too large" fallback HTML block. The link path is
/// assembled from `repo_name`, `branch`, and `file_name` after html-escaping.
/// We do NOT percent-encode: callers are expected to pass URL-safe values
/// (git branch and repo names that follow normal slug conventions, and one
/// of the fixed README filenames). Slashes in branch names are intentionally
/// preserved as path separators.
fn too_large_notice_with_repo(repo_name: &str, branch: &str, file_name: &str) -> String {
    format!(
        "<p><em>README too large to render. \
@@ -412,6 +425,15 @@ mod tests {
    }

    #[test]
    fn plain_text_post_render_cap_trips() {
        // 500 KiB of '&' → ~2.5 MiB of "&amp;" inside a <pre>.
        let bytes = vec![b'&'; 500 * 1024];
        let (repo, _tmp) = repo_with_files(&[("README", &bytes)]);
        let r = load(&repo).unwrap();
        assert!(r.html.contains("too large"), "expected plain-text bomb to trip cap");
    }

    #[test]
    fn binary_blob_returns_none() {
        let (repo, _tmp) = repo_with_files(&[("README.md", &[0u8, 1, 2, 3, 0xff, 0xfe])]);
        assert!(load(&repo).is_none());