Some updates

This commit is contained in:
Luke Street
2024-09-07 17:31:06 -06:00
parent 4deabfa8b2
commit 7b47efd66d
11 changed files with 361 additions and 199 deletions
Generated
+99 -113
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -13,7 +13,7 @@ grass = "0.13"
image = "0.25"
lightningcss = "1.0.0-alpha"
mime = "0.3"
minijinja = { version = "2.2", features = ["loader"] }
minijinja = { version = "2.2", features = ["loader", "json"] }
minijinja-autoreload = "2.2"
moka = { version = "0.12", features = ["future"] }
objdiff-core = { git = "https://github.com/encounter/objdiff", tag = "v2.0.0-beta.6", features = ["bindings"] }
@@ -36,6 +36,6 @@ tower = { version = "0.4", features = ["full"] }
tower-http = { version = "0.5", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
treemap = "0.3"
url = { version = "2.5", features = ["serde"] }
zip = { version = "2.2", default-features = false, features = ["deflate-flate2"] }
zstd = "0.13"
+3 -2
View File
@@ -255,7 +255,8 @@ $unit-size: 0.75rem;
margin-bottom: var(--pico-spacing);
touch-action: none;
// Accessed via JS
--font-weight: 400;
--font-size: 1rem;
--font-weight: normal;
--font-size: 0.875rem;
--font-family: var(--pico-font-family);
--tooltip-background: var(--pico-tooltip-background-color);
}
+59 -28
View File
@@ -10,48 +10,75 @@ type Unit = {
const unitBounds = (unit: Unit, width: number, height: number) => {
return {
x: (unit.x / 100) * width,
y: (unit.y / 100) * height,
w: (unit.w / 100) * width,
h: (unit.h / 100) * height,
x: unit.x * width,
y: unit.y * height,
w: unit.w * width,
h: unit.h * height,
};
}
const TOOLTIP_PADDING = 10;
const TOOLTIP_MARGIN = 10;
const BORDER_RADIUS = 5;
const PADDING_W = 10;
const PADDING_H = 5;
const MARGIN = 5;
const drawTooltip = (ctx: CanvasRenderingContext2D, unit: Unit, width: number, height: number) => {
const style = getComputedStyle(ctx.canvas);
const fontWeight = style.getPropertyValue('--font-weight') || 'normal';
const fontSize = style.getPropertyValue('--font-size') || '16px';
const fontFamily = style.getPropertyValue('--font-family') || 'sans-serif';
const tooltipBackground = style.getPropertyValue('--tooltip-background') || "#fff"
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
ctx.textBaseline = "middle";
const {x, y, w, h} = unitBounds(unit, width, height);
const text = `${unit.name}${unit.fuzzy_match_percent.toFixed(2)}%`;
const m = ctx.measureText(text);
const hw = m.actualBoundingBoxRight + m.actualBoundingBoxLeft + TOOLTIP_PADDING * 2;
const bh = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent + TOOLTIP_PADDING * 2;
let bx = x + (w - hw) / 2 + TOOLTIP_PADDING;
let by = y - TOOLTIP_MARGIN;
const bw = m.actualBoundingBoxRight + m.actualBoundingBoxLeft + PADDING_W * 2;
const bh = m.fontBoundingBoxAscent + m.fontBoundingBoxDescent + PADDING_H * 2;
const margin = isTouch ? MARGIN * 2 : MARGIN;
let bx = x + (w - bw) / 2 + PADDING_W;
let by = y - bh - margin;
let invY = false;
if (bx + bw > width) {
bx = width - bw;
}
if (bx < 0) {
bx = 0;
}
if (bx + hw > width) {
bx = width - hw;
if (by < 0) {
by = y + h + margin;
invY = true;
}
if (by - bh < 0) {
by = y + h + bh + TOOLTIP_MARGIN;
ctx.fillStyle = tooltipBackground;
ctx.beginPath();
ctx.roundRect(bx, by, bw, bh, BORDER_RADIUS);
// Arrow
const ax = x + w / 2;
if (invY) {
ctx.moveTo(ax, y + h);
ctx.lineTo(ax + margin, y + h + margin);
ctx.lineTo(ax - margin, y + h + margin);
} else {
ctx.moveTo(ax, y);
ctx.lineTo(ax + margin, y - margin);
ctx.lineTo(ax - margin, y - margin);
}
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.fillRect(bx, by - m.actualBoundingBoxAscent - TOOLTIP_PADDING * 2, hw, bh);
ctx.fill();
ctx.fillStyle = "#000";
ctx.fillText(text, bx + TOOLTIP_PADDING, by - TOOLTIP_PADDING);
ctx.fillText(text, bx + PADDING_W, by + bh / 2);
};
let hovered = null;
let dirty = false;
let isTouch = false;
const draw = (canvas: HTMLCanvasElement, units: Unit[]) => {
if (!canvas.getContext) {
return;
}
const ratio = window.devicePixelRatio;
const {width, height} = canvas.getBoundingClientRect();
const ratio = window.devicePixelRatio;
const renderWidth = width * ratio;
const renderHeight = height * ratio;
if (!dirty && canvas.width === renderWidth && canvas.height === renderHeight) {
@@ -59,25 +86,23 @@ const draw = (canvas: HTMLCanvasElement, units: Unit[]) => {
return;
}
dirty = false;
// High DPI support
if (canvas.width !== renderWidth || canvas.height !== renderHeight) {
canvas.width = renderWidth;
canvas.height = renderHeight;
}
const style = getComputedStyle(canvas);
const fontWeight = style.getPropertyValue('--font-weight') || 'normal';
const fontSize = style.getPropertyValue('--font-size') || '16px';
const fontFamily = style.getPropertyValue('--font-family') || 'sans-serif';
const ctx = canvas.getContext("2d");
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0); // Scale to device pixel ratio
ctx.clearRect(0, 0, width, height);
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
for (const unit of units) {
const {x, y, w, h} = unitBounds(unit, width, height);
ctx.fillStyle = unit.color;
ctx.fillRect(x, y, w, h);
ctx.strokeRect(x, y, w, h);
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.fill();
ctx.stroke();
}
if (hovered) {
const {x, y, w, h} = unitBounds(hovered, width, height);
@@ -123,9 +148,15 @@ const drawGraph = (id: string, units: Unit[]) => {
dirty = true;
queueDraw();
};
canvas.addEventListener("mousemove", handleHover);
canvas.addEventListener("mousemove", (e) => {
isTouch = false;
handleHover(e);
});
canvas.addEventListener("mouseleave", handleLeave);
canvas.addEventListener("touchmove", (e) => handleHover(e.touches[0]));
canvas.addEventListener("touchmove", (e) => {
isTouch = true;
handleHover(e.touches[0]);
});
canvas.addEventListener("touchend", handleLeave);
draw(canvas, units);
};
+4 -4
View File
@@ -73,11 +73,11 @@ fn transform(
) -> Result<CodegenReturn> {
let source_type = SourceType::from_path(path)?;
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, &source_text, source_type).parse();
let parsed = Parser::new(&allocator, source_text, source_type).parse();
let program = allocator.alloc(parsed.program);
let (symbols, scopes) = SemanticBuilder::new(&source_text, source_type)
.build(&program)
let (symbols, scopes) = SemanticBuilder::new(source_text, source_type)
.build(program)
.semantic
.into_symbol_table_and_scope_tree();
@@ -91,7 +91,7 @@ fn transform(
&allocator,
path,
source_type,
&source_text,
source_text,
parsed.trivias.clone(),
transform_options,
)
+53 -4
View File
@@ -1,10 +1,12 @@
use std::{str::FromStr, sync::Arc};
use std::{convert::Infallible, net::SocketAddr, str::FromStr, sync::Arc};
use axum::{
http::{header, HeaderMap, StatusCode},
async_trait,
extract::{ConnectInfo, FromRequestParts, OriginalUri},
http::{header, request::Parts, HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
routing::get,
Router,
Extension, Router,
};
use bytes::BytesMut;
use mime::Mime;
@@ -14,9 +16,9 @@ use crate::AppState;
mod css;
mod graph;
mod js;
mod project;
mod report;
mod js;
pub fn build_router() -> Router<AppState> {
Router::new()
@@ -76,3 +78,50 @@ impl<T: Message> IntoResponse for Protobuf<T> {
([(header::CONTENT_TYPE, APPLICATION_PROTOBUF)], bytes.freeze()).into_response()
}
}
/// Extractor for the full URI of the request, including the scheme and authority.
/// Uses the `x-forwarded-proto` and `x-forwarded-host` headers if present.
pub struct FullUri(pub Uri);
#[async_trait]
impl<S> FromRequestParts<S> for FullUri
where S: Send + Sync
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let uri = Extension::<OriginalUri>::from_request_parts(parts, state)
.await
.map_or_else(|_| parts.uri.clone(), |Extension(OriginalUri(uri))| uri);
let mut builder = Uri::builder();
if let Some(scheme) =
parts.headers.get("x-forwarded-proto").and_then(|value| value.to_str().ok())
{
builder = builder.scheme(scheme);
} else if let Some(scheme) = uri.scheme().cloned() {
builder = builder.scheme(scheme);
} else {
// TODO: native https?
builder = builder.scheme("http");
}
if let Some(host) =
parts.headers.get("x-forwarded-host").and_then(|value| value.to_str().ok())
{
builder = builder.authority(host);
} else if let Some(host) =
parts.headers.get(header::HOST).and_then(|value| value.to_str().ok())
{
builder = builder.authority(host);
} else if let Some(authority) = uri.authority().cloned() {
builder = builder.authority(authority);
} else if let Ok(ConnectInfo(socket_addr)) =
ConnectInfo::<SocketAddr>::from_request_parts(parts, state).await
{
builder = builder.authority(socket_addr.to_string());
}
if let Some(path_and_query) = uri.path_and_query().cloned() {
builder = builder.path_and_query(path_and_query);
}
Ok(FullUri(builder.build().unwrap_or(uri)))
}
}
+73 -32
View File
@@ -2,21 +2,23 @@ use std::{iter, str::FromStr, time::Instant};
use anyhow::{Context, Result};
use axum::{
extract::{Path, Query, State},
http::{header, HeaderMap, StatusCode},
extract::{ Path, Query, State},
http::{header, HeaderMap, StatusCode, Uri},
response::{Html, IntoResponse, Response},
Json,
};
use image::ImageFormat;
use mime::Mime;
use objdiff_core::bindings::report::Measures;
use objdiff_core::bindings::report::{Measures, ReportCategory};
use serde::{Deserialize, Serialize};
use url::Url;
use super::{graph::layout_units, parse_accept, AppError, Protobuf, PROTOBUF};
use super::{graph::layout_units, parse_accept, AppError, FullUri, Protobuf, PROTOBUF};
use crate::{
handlers::graph::{render_image, render_svg, unit_color},
models::{Project, ProjectInfo, ReportFile},
templates::render,
util::UrlExt,
AppState,
};
@@ -55,12 +57,14 @@ struct ReportTemplateContext<'a> {
version: &'a str,
measures: TemplateMeasures,
units: &'a [ReportTemplateUnit<'a>],
versions: &'a [String],
versions: &'a [ReportTemplateVersion<'a>],
prev_commit: Option<&'a str>,
next_commit: Option<&'a str>,
categories: &'a [ReportCategoryItem<'a>],
current_category: &'a ReportCategoryItem<'a>,
units_json: String,
canonical_path: &'a str,
canonical_url: &'a str,
image_url: &'a str,
}
#[derive(Serialize)]
@@ -74,14 +78,17 @@ pub struct ReportTemplateUnit<'a> {
h: f32,
}
#[derive(Serialize, Copy, Clone)]
#[derive(Serialize, Clone)]
struct ReportCategoryItem<'a> {
id: &'a str,
name: &'a str,
path: String,
}
impl Default for ReportCategoryItem<'static> {
fn default() -> Self { Self { id: "all", name: "All" } }
#[derive(Serialize, Clone)]
struct ReportTemplateVersion<'a> {
id: &'a str,
path: String,
}
/// Duplicate of Measures to avoid omitting empty fields
@@ -159,13 +166,14 @@ fn extract_extension(params: ReportParams) -> (ReportParams, Option<String>) {
} else if let Some((repo, ext)) = params.repo.rsplit_once('.') {
return (ReportParams { repo: repo.to_string(), ..params }, Some(ext.to_string()));
}
return (params, None);
(params, None)
}
pub async fn get_report(
Path(params): Path<ReportParams>,
Query(query): Query<ReportQuery>,
headers: HeaderMap,
FullUri(uri): FullUri,
State(state): State<AppState>,
) -> Result<Response, AppError> {
let start = Instant::now();
@@ -225,9 +233,10 @@ pub async fn get_report(
&report,
&project_info,
measures,
&current_category,
current_category,
&units,
&state,
uri,
)?;
let elapsed = start.elapsed();
rendered = rendered.replace("[[time]]", &format!("{}ms", elapsed.as_millis()));
@@ -274,15 +283,15 @@ const EMPTY_MEASURES: Measures = Measures {
fn apply_category<'a>(
report: &'a ReportFile,
query: &ReportQuery,
) -> Result<(&'a Measures, ReportCategoryItem<'a>, Vec<ReportTemplateUnit<'a>>)> {
) -> Result<(&'a Measures, Option<&'a ReportCategory>, Vec<ReportTemplateUnit<'a>>)> {
let mut measures = report.report.measures.as_ref().unwrap_or(&EMPTY_MEASURES);
let mut current_category = ReportCategoryItem::default();
let mut current_category = None;
let mut category_id_filter = None;
if let Some(category) =
query.category.as_ref().and_then(|id| report.report.categories.iter().find(|c| c.id == *id))
{
measures = category.measures.as_ref().unwrap_or(&EMPTY_MEASURES);
current_category = ReportCategoryItem { id: &category.id, name: &category.name };
current_category = Some(category);
category_id_filter = Some(category.id.clone());
}
let (w, h) = query.size();
@@ -313,10 +322,10 @@ fn apply_category<'a>(
name: &unit.name,
fuzzy_match_percent: match_percent,
color: unit_color(match_percent),
x: bounds.x * 100.0,
y: bounds.y * 100.0,
w: bounds.w * 100.0,
h: bounds.h * 100.0,
x: bounds.x,
y: bounds.y,
w: bounds.w,
h: bounds.h,
}
})
.collect();
@@ -327,35 +336,67 @@ fn render_template(
report: &ReportFile,
project_info: &ProjectInfo,
measures: &Measures,
current_category: &ReportCategoryItem,
current_category: Option<&ReportCategory>,
units: &[ReportTemplateUnit],
state: &AppState,
uri: Uri,
) -> Result<String> {
let categories = iter::once(ReportCategoryItem::default())
.chain(
report
.report
.categories
let request_url = Url::parse(&uri.to_string()).context("Failed to parse URI")?;
let project_base_path =
format!("/{}/{}", project_info.project.owner, project_info.project.repo);
let canonical_url = request_url.with_path(&format!(
"/{}/{}/{}/{}",
project_info.project.owner, project_info.project.repo, report.version, report.commit.sha
));
let image_url = canonical_url.with_path(&format!("{}.png", canonical_url.path()));
let versions = project_info
.report_versions
.iter()
.map(|c| ReportCategoryItem { id: &c.id, name: &c.name }),
)
.map(|version| {
let version_url = request_url.with_path(&format!(
"/{}/{}/{}/{}",
project_info.project.owner, project_info.project.repo, version, report.commit.sha
));
ReportTemplateVersion { id: version, path: version_url.path_and_query().to_string() }
})
.collect::<Vec<_>>();
let units_json = serde_json::to_string(&units).context("Failed to serialize units")?;
let all_url = canonical_url.set_query("category", None);
let all_category =
ReportCategoryItem { id: "all", name: "All", path: all_url.path_and_query().to_string() };
let current_category = current_category
.map(|c| {
let path =
canonical_url.set_query("category", Some(&c.id)).path_and_query().to_string();
ReportCategoryItem { id: &c.id, name: &c.name, path }
})
.unwrap_or_else(|| all_category.clone());
let categories = iter::once(all_category)
.chain(report.report.categories.iter().map(|c| {
let path =
canonical_url.set_query("category", Some(&c.id)).path_and_query().to_string();
ReportCategoryItem { id: &c.id, name: &c.name, path }
}))
.collect::<Vec<_>>();
render(&state.templates, "report.html", ReportTemplateContext {
project: &report.project,
project_name: &report.project.name(),
project_short_name: &report.project.short_name(),
project_short_name: report.project.short_name(),
project_url: &report.project.repo_url(),
project_path: &format!("/{}/{}", report.project.owner, report.project.repo),
project_path: &project_base_path,
commit: &report.commit.sha,
version: &report.version,
measures: TemplateMeasures::from(measures),
units,
versions: &project_info.report_versions,
versions: &versions,
prev_commit: project_info.prev_commit.as_deref(),
next_commit: project_info.next_commit.as_deref(),
categories: &categories,
current_category,
units_json,
current_category: &current_category,
canonical_path: canonical_url.path_and_query(),
canonical_url: canonical_url.as_ref(),
image_url: image_url.as_ref(),
})
}
+5 -1
View File
@@ -5,6 +5,7 @@ mod github;
mod handlers;
mod models;
mod templates;
mod util;
use std::{
fs::File,
@@ -51,7 +52,10 @@ async fn main() {
// Run our service
let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, state.config.server.port));
tracing::info!("Listening on {}", addr);
axum::serve(TcpListener::bind(addr).await.expect("bind error"), app(state).into_make_service())
axum::serve(
TcpListener::bind(addr).await.expect("bind error"),
app(state).into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal())
.await
.expect("server error");
+50
View File
@@ -0,0 +1,50 @@
use url::Url;
pub trait UrlExt {
fn set_query(&self, key: &str, value: Option<&str>) -> Url;
fn with_path(&self, path: &str) -> Url;
fn path_and_query(&self) -> &str;
}
impl UrlExt for Url {
#[inline]
fn set_query(&self, key: &str, value: Option<&str>) -> Url {
let mut out = self.clone();
let mut pairs = out.query_pairs_mut();
pairs.clear();
let mut updated = false;
for (k, v) in self.query_pairs() {
if k == key {
if let Some(value) = value {
if value.is_empty() {
pairs.append_key_only(&k);
} else {
pairs.append_pair(&k, value);
}
}
updated = true;
} else if v.is_empty() {
pairs.append_key_only(&k);
} else {
pairs.append_pair(&k, &v);
}
}
if !updated {
if let Some(value) = value {
pairs.append_pair(key, value);
}
}
drop(pairs);
out
}
#[inline]
fn with_path(&self, path: &str) -> Url {
let mut out = self.clone();
out.set_path(path);
out
}
#[inline]
fn path_and_query(&self) -> &str { &self[url::Position::BeforePath..] }
}
+8 -8
View File
@@ -7,15 +7,15 @@
<meta name="darkreader-lock">
<meta name="description" content="Decompilation progress report for {{ project_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %}">
<title>{{ project_short_name }}{% if current_category.id != "all" %} ({{ current_category.name }}){% endif %} • Progress Report</title>
<link rel="stylesheet" href="/css/main.min.css?2">
<link rel="stylesheet" href="/css/main.min.css?3">
<script src="https://unpkg.com/htmx.org@2.0.2"
integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
crossorigin="anonymous"></script>
<script src="/js/graph.min.js?2"></script>
<meta property="og:type" content="website">
<script src="/js/graph.min.js?5"></script>
<meta property="og:title" content="{{ project_short_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %} is {{ measures.matched_code_percent | round(2) }}% decompiled">
<meta property="og:description" content="Decompilation progress report for {{ project_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %}">
<meta property="og:image" content="https://decomp.dev{{ project_path | safe }}/{{ version }}/{{ commit }}.png">
<meta property="og:image" content="{{ image_url | safe }}">
<meta property="og:url" content="{{ canonical_url | safe }}">
</head>
<body>
<header>
@@ -37,7 +37,7 @@
<summary>{{ version }}</summary>
<ul dir="rtl">
{% for version in versions %}
<li><a href="{{ project_path | safe }}/{{ version }}">{{ version }}</a></li>
<li><a href="{{ version.path | safe }}">{{ version.id }}</a></li>
{% endfor %}
</ul>
</details>
@@ -94,17 +94,17 @@
<summary>{{ current_category.name }}</summary>
<ul>
{% for category in categories %}
<li><a href="?category={{ category.id }}">{{ category.name }}</a></li>
<li><a href="{{ category.path | safe }}">{{ category.name }}</a></li>
{% endfor %}
</ul>
</details>
{% endif %}
<script>
document.write('<canvas id="graph" width="100%" data-hx-preserve="true"></canvas>');
drawGraph("graph", {{ units_json | safe }});
drawGraph("graph", {{ units | tojson }});
</script>
<noscript>
<img id="graph" src="{{ project_path | safe }}/{{ version }}/{{ commit }}.png" alt="Progress graph">
<img id="graph" src="{{ image_url | safe }}" alt="Progress graph">
</noscript>
<h6 class="report-header">Links</h6>
<div>
+1 -1
View File
@@ -2,6 +2,6 @@
<svg viewBox="0 0 {{ w }} {{ h }}" xmlns="http://www.w3.org/2000/svg">
<style>.unit { stroke: #000; stroke-width: 1; }</style>
{% for unit in units %}
<rect class="unit" width="{{ unit.w }}%" height="{{ unit.h }}%" x="{{ unit.x }}%" y="{{ unit.y }}%" fill="{{ unit.color }}" />
<rect class="unit" width="{{ unit.w * 100.0 }}%" height="{{ unit.h * 100.0 }}%" x="{{ unit.x * 100.0 }}%" y="{{ unit.y * 100.0 }}%" fill="{{ unit.color }}" />
{% endfor %}
</svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 386 B