Krolyxon
Commit 4e844bb

ferreniumsrchandlersblob.rs

215 lines · 6.9 KB · Rust

1//! `/{repo}/blob/{rev}/{path…}` — a file: highlighted code with line
2//! anchors, rendered Markdown, inline images.
3
4use std::sync::Arc;
5
6use askama::Template;
7use axum::http::StatusCode;
8use axum::response::Response;
9use gix::bstr::ByteSlice;
10
11use super::repo::{Active, Crumb, Ctx, RepoNav, crumbs, redirect, set_cache_headers};
12use super::{Layout, render};
13use crate::cache::Key;
14use crate::git::tree::{self, EntryKind, PathTarget};
15use crate::render::markdown::{self, Links};
16use crate::render::{encode_path, highlight, human_size};
17
18/// Extensions shown inline as images (served by the raw endpoint).
19const IMAGES: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "ico"];
20
21pub enum Body {
22 Code { lines: Arc<Vec<String>> },
23 Markdown { html: Arc<str> },
24 Image { src: String },
25 Symlink { target: String, url: Option<String> },
26 Binary,
27 TooLarge,
28 Empty,
29}
30
31pub struct ViewToggle {
32 pub preview_url: String,
33 pub source_url: String,
34 pub source: bool,
35}
36
37#[derive(Template)]
38#[template(path = "blob.html")]
39pub struct BlobPage {
40 pub layout: Layout,
41 pub nav: RepoNav,
42 pub crumbs: Vec<Crumb>,
43 pub here: Option<String>,
44 /// `412 lines`, `12.4 KB`, `Rust`.
45 pub facts: Vec<String>,
46 pub raw_url: String,
47 pub log_url: String,
48 pub download_url: String,
49 pub toggle: Option<ViewToggle>,
50 pub body: Body,
51}
52
53pub fn page(ctx: &Ctx, segments: &[Vec<u8>]) -> Response {
54 let (rev, path) = match ctx.resolve(segments) {
55 Ok(found) => found,
56 Err(res) => return *res,
57 };
58 let (oid, kind) = match tree::lookup(&ctx.git, rev.commit, &path) {
59 Ok(Some(PathTarget::Blob(oid, kind))) => (oid, kind),
60 Ok(Some(PathTarget::Tree(_))) => return redirect(&ctx.url("tree", &rev.name, &path)),
61 Ok(Some(PathTarget::Submodule(_))) => return ctx.not_found("PATH IS A SUBMODULE"),
62 Ok(None) => return ctx.not_found(&format!("PATH NOT FOUND IN {}", rev.name)),
63 Err(e) => return ctx.internal_error(e),
64 };
65 let source = ctx.query.view.as_deref() == Some("source");
66 let etag = ctx.etag(("blob", oid, &rev.name, source));
67 let immutable = rev.is_immutable();
68 if let Some(res) = ctx.not_modified(&etag, immutable) {
69 return res;
70 }
71 let size = match tree::size(&ctx.git, oid) {
72 Ok(size) => size,
73 Err(e) => return ctx.internal_error(e),
74 };
75
76 let path_str = path.to_str_lossy().into_owned();
77 let name = path_str.rsplit('/').next().unwrap_or(&path_str).to_string();
78 let dir = path.rsplit_once_str("/").map_or(&b""[..], |(d, _)| d);
79 let raw_url = ctx.url("raw", &rev.name, &path);
80 let mut lines_fact = None;
81 let mut language = None;
82 let mut toggle = None;
83 let ext = name
84 .rsplit_once('.')
85 .map(|(_, e)| e.to_ascii_lowercase())
86 .unwrap_or_default();
87
88 let body = if size == 0 {
89 Body::Empty
90 } else if IMAGES.contains(&ext.as_str()) && kind != EntryKind::Symlink {
91 Body::Image {
92 src: raw_url.clone(),
93 }
94 } else if size > ctx.config.ui.max_blob_kb * 1024 {
95 Body::TooLarge
96 } else {
97 let data = match tree::blob(&ctx.git, oid) {
98 Ok(data) => data,
99 Err(e) => return ctx.internal_error(e),
100 };
101 if kind == EntryKind::Symlink {
102 let target = data.to_str_lossy().into_owned();
103 let url = resolve_link(dir, &data).map(|p| ctx.url("blob", &rev.name, &p));
104 Body::Symlink { target, url }
105 } else if data[..data.len().min(8000)].contains(&0) {
106 Body::Binary
107 } else {
108 let text = String::from_utf8_lossy(&data);
109 lines_fact = Some(line_count(&text));
110 let is_md = markdown::is_markdown(&name);
111 if is_md {
112 let base = ctx.url("blob", &rev.name, &path);
113 toggle = Some(ViewToggle {
114 source_url: format!("{base}?view=source"),
115 preview_url: base,
116 source,
117 });
118 }
119 if is_md && !source {
120 let links = Links {
121 repo_url: ctx.repo_url(),
122 rev: encode_path(&rev.name),
123 dir: dir.to_vec(),
124 };
125 let key = Key::Markdown {
126 blob: oid,
127 context: links.key(),
128 };
129 let html = ctx.state.cache.html(key, || {
130 Ok::<_, std::convert::Infallible>(markdown::render(&text, &links))
131 });
132 Body::Markdown {
133 html: html.unwrap_or_else(|never| match never {}),
134 }
135 } else {
136 let (lines, lang) = highlight::cached_lines(
137 &ctx.state.cache,
138 oid,
139 &path_str,
140 &text,
141 ctx.config.ui.max_highlight_kb * 1024,
142 );
143 language = lang.map(str::to_string);
144 Body::Code { lines }
145 }
146 }
147 };
148 let facts = [lines_fact, Some(human_size(size)), language]
149 .into_iter()
150 .flatten()
151 .collect();
152
153 let (crumbs, _) = crumbs(ctx, &rev.name, &path);
154 let page = BlobPage {
155 layout: Layout::new(
156 ctx.state,
157 ctx.config,
158 Some(&format!("{path_str} · {}", ctx.repo.name)),
159 ),
160 nav: ctx.nav(Active::Tree, Some(&rev), |name| {
161 ctx.url("blob", name, &path)
162 }),
163 crumbs,
164 here: Some(name),
165 facts,
166 download_url: format!("{raw_url}?download"),
167 log_url: ctx.url("log", &rev.name, &path),
168 raw_url,
169 toggle,
170 body,
171 };
172 let mut res = render(StatusCode::OK, &page);
173 set_cache_headers(&mut res, &etag, immutable);
174 res
175}
176
177fn line_count(text: &str) -> String {
178 let n = text.lines().count();
179 format!("{n} {}", if n == 1 { "line" } else { "lines" })
180}
181
182/// Resolve a relative symlink target against the link's directory. `None`
183/// for absolute targets or ones leaving the repository.
184fn resolve_link(dir: &[u8], target: &[u8]) -> Option<Vec<u8>> {
185 if target.starts_with(b"/") {
186 return None;
187 }
188 let mut parts: Vec<&[u8]> = dir
189 .split(|b| *b == b'/')
190 .filter(|p| !p.is_empty())
191 .collect();
192 for part in target.split(|b| *b == b'/') {
193 match part {
194 b"" | b"." => {}
195 b".." => {
196 parts.pop()?;
197 }
198 part => parts.push(part),
199 }
200 }
201 Some(parts.join(&b'/'))
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn symlink_targets() {
210 assert_eq!(resolve_link(b"a/b", b"../c.txt").unwrap(), b"a/c.txt");
211 assert_eq!(resolve_link(b"", b"./x").unwrap(), b"x");
212 assert!(resolve_link(b"", b"../x").is_none());
213 assert!(resolve_link(b"a", b"/etc/passwd").is_none());
214 }
215}