v0.4.61: asset cache fix, PNG-enabled iPXE, composed PXE logo
Two real issues v0.4.6 left on the table: Asset caching: - index.html now interpolates the running OpenPXE version into every asset URL as `?v=<version>` (app.css, app.js, logo.svg). Combined with `Cache-Control: no-cache, must-revalidate` on the asset handlers, browsers and intermediary proxies are forced to fetch fresh on every upgrade. Without this, last release's bundled JS kept serving the old UI even after the operator pulled the new image — invisible to anyone who only checks the version chip in the footer (which is dynamic). - The Cache-Control header is also applied to logo.svg and loader.svg so a logo upload reflects immediately rather than after a hard refresh. Real-image PXE menu logo (matches iVentoy now): - New Dockerfile stage `ipxe-build` clones the iPXE source and compiles all four binaries (undionly.kpxe, snponly.efi for x86_64/i386, snponly.efi for arm64 via gcc-aarch64-linux-gnu) with IMAGE_PNG + CONSOLE_FRAMEBUFFER + CONSOLE_VESAFB enabled. Replaces the boot.ipxe.org fetch — those binaries are built without PNG support, which is why v0.4.6's `console --picture` line silently no-op'd. - `iso-store::pxe_logo::compose_pxe_logo` decodes any operator upload (PNG / JPEG / WebP / GIF), downscales-to-fit if larger than 600×200, and pastes it onto a transparent 1024×768 canvas centered horizontally with a 64-pixel top margin. iPXE paints the result at 1:1 on the typical VESA framebuffer, giving the iVentoy-style centered-logo look regardless of the operator's source dimensions. - GET /branding/pxe-logo now returns the composed PNG. wimboot still fetches from ipxe/wimboot's GitHub release (separately signed). - Dropped the ASCII OpenPXE wordmark from render_menu — once the real image paints, the banner would duplicate it visually. iPXE builds without PNG (none of ours after this release, but a third- party undionly might) simply show the menu without a logo, which is the right graceful-degradation outcome. Quality: - 142 tests passing (was 138 in v0.4.6): +4 pxe_logo unit tests covering canvas dimensions, centered-top placement, oversize downscale, and unsupported-bytes error handling; existing integration tests updated to verify the 1024×768 IHDR header from the composed PNG instead of round-tripping the raw upload. - cargo clippy --workspace --all-targets clean. - Image dependency: `image = "0.25"` with only `png/jpeg/webp/gif` features enabled. No new transitive C deps. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
55f4765a20
commit
1419309a2d
@@ -1755,11 +1755,26 @@ async fn pxe_logo_404_when_no_custom_logo_configured() {
|
||||
assert!(text.contains("no custom logo"), "got: {text}");
|
||||
}
|
||||
|
||||
/// Build a tiny valid PNG via the `image` crate. The v0.4.61 PXE-logo
|
||||
/// compositor decodes whatever the operator uploaded — hand-rolled
|
||||
/// PNGs with handwritten CRCs are too easy to break; let the encoder
|
||||
/// produce something it can later decode.
|
||||
fn tiny_png() -> Vec<u8> {
|
||||
use image::{DynamicImage, ImageBuffer, ImageFormat, Rgb};
|
||||
use std::io::Cursor;
|
||||
let buf: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_pixel(8, 8, Rgb([0, 180, 220]));
|
||||
let mut out = Vec::with_capacity(256);
|
||||
DynamicImage::ImageRgb8(buf)
|
||||
.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
|
||||
.unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pxe_logo_404_when_uploaded_logo_is_svg() {
|
||||
// iPXE can't rasterize SVG, so an SVG upload deliberately doesn't
|
||||
// light up the PXE menu's `console --picture` overlay — the ASCII
|
||||
// wordmark in render_menu stands in instead.
|
||||
// light up the PXE menu's `console --picture` overlay — the menu
|
||||
// simply paints without a logo.
|
||||
let (state, _dir) = build_state().await;
|
||||
state
|
||||
.branding
|
||||
@@ -1777,11 +1792,15 @@ async fn pxe_logo_404_when_uploaded_logo_is_svg() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pxe_logo_serves_raster_with_correct_mime() {
|
||||
async fn pxe_logo_composes_to_1024x768_png() {
|
||||
// v0.4.61: the endpoint no longer serves the raw upload — it
|
||||
// composes the operator's logo into a fixed 1024×768 canvas so
|
||||
// the iPXE menu always paints at consistent dimensions.
|
||||
let (state, _dir) = build_state().await;
|
||||
let png = tiny_png();
|
||||
state
|
||||
.branding
|
||||
.set_logo("image/png", "png", b"\x89PNG\r\n\x1a\nfake-png-bytes")
|
||||
.set_logo("image/png", "png", &png)
|
||||
.unwrap();
|
||||
let app = build_router(state);
|
||||
let res = app
|
||||
@@ -1805,7 +1824,14 @@ async fn pxe_logo_serves_raster_with_correct_mime() {
|
||||
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
// PNG signature.
|
||||
assert!(body.starts_with(b"\x89PNG"), "PNG header missing");
|
||||
// IHDR chunk lives at bytes 8..29; width is bytes 16..20, height
|
||||
// 20..24 in big-endian u32. The composed canvas should be 1024×768.
|
||||
let width = u32::from_be_bytes([body[16], body[17], body[18], body[19]]);
|
||||
let height = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
||||
assert_eq!(width, 1024, "compose should pin width to 1024");
|
||||
assert_eq!(height, 768, "compose should pin height to 768");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1814,9 +1840,10 @@ async fn pxe_logo_endpoint_is_public_after_admin_setup() {
|
||||
// must stay reachable once the admin has been bootstrapped. The
|
||||
// auth allowlist gates `/api/*` only.
|
||||
let (state, _dir) = build_state().await;
|
||||
let png = tiny_png();
|
||||
state
|
||||
.branding
|
||||
.set_logo("image/png", "png", b"\x89PNG\r\n\x1a\nfake")
|
||||
.set_logo("image/png", "png", &png)
|
||||
.unwrap();
|
||||
let app = build_router(state);
|
||||
// Configure an admin so the middleware kicks in.
|
||||
|
||||
Reference in New Issue
Block a user