Skip to main content

max / balanced_breakfast

bb: remediate deepaudit 07-21 (srcset bypass, pdf wrap, clippy) - Sanitizer: check each srcset candidate URL independently. A remote URL in any non-first candidate ("a.jpg 1x, https://tracker/px 2x") slipped past the whole-value scheme test, re-opening the tracking-pixel beacon. SERIOUS, 2nd consecutive run. - bb-pdf wrap(): hard-break a token wider than the column on char boundaries instead of overflowing the page edge; regression test added. - Restore clippy-green under toolchain 1.95.0 (remaining literal-separator and format!-inlining warnings in test code). Not addressed here: the keyring 3->4 Linux credential-store swap (SERIOUS) needs a design call between a re-config prompt, keeping the file fallback, or pinning the store -- left for Max. aes-gcm already at 0.11. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 19:35 UTC
Commit: 1e40478d01c16afbb98e415ccd02688b684f505e
Parent: d7ef3a6
6 files changed, +83 insertions, -13 deletions
@@ -968,7 +968,7 @@ mod tests {
968 968 "source_name".into(),
969 969 Dynamic::from("Test Source".to_string()),
970 970 );
971 - meta_map.insert("published_at".into(), Dynamic::from(1700000000_i64));
971 + meta_map.insert("published_at".into(), Dynamic::from(1_700_000_000_i64));
972 972 meta_map.insert("score".into(), Dynamic::from(42_i64));
973 973
974 974 let tags: rhai::Array = vec![
@@ -983,7 +983,7 @@ mod tests {
983 983
984 984 let item = dynamic_to_feed_item(Dynamic::from(map), "src").unwrap();
985 985 assert_eq!(item.meta.source_name, "Test Source");
986 - assert_eq!(item.meta.published_at, 1700000000);
986 + assert_eq!(item.meta.published_at, 1_700_000_000);
987 987 assert_eq!(item.meta.score, Some(42));
988 988 assert_eq!(item.meta.tags, vec!["rust", "tech"]);
989 989 }
@@ -239,7 +239,7 @@ mod tests {
239 239 #[test]
240 240 fn busser_id_display() {
241 241 let id = BusserId::new("hn");
242 - assert_eq!(format!("{}", id), "hn");
242 + assert_eq!(format!("{id}"), "hn");
243 243 }
244 244
245 245 #[test]
@@ -1476,7 +1476,7 @@ async fn circuit_breaker_trips_at_threshold() {
1476 1476 // Record failures up to threshold - 1: should not trip
1477 1477 for i in 0..(CIRCUIT_BREAKER_THRESHOLD - 1) {
1478 1478 let tripped = feeds
1479 - .record_fetch_failure(feed.id, &format!("error {}", i))
1479 + .record_fetch_failure(feed.id, &format!("error {i}"))
1480 1480 .await
1481 1481 .unwrap();
1482 1482 assert!(!tripped, "should not trip at failure {}", i + 1);
@@ -340,7 +340,7 @@ mod tests {
340 340 fn make_item(source: &str, id: &str, published_at: i64) -> FeedItem {
341 341 FeedItem::new(
342 342 FeedItemId::new(source, id),
343 - BiteDisplay::new("author", format!("text for {}", id)),
343 + BiteDisplay::new("author", format!("text for {id}")),
344 344 FeedItemContent::new(),
345 345 FeedItemMeta::new(source, published_at),
346 346 )
@@ -319,6 +319,20 @@ fn wrap(text: &str, style: Style, max_width_mm: f32) -> Vec<String> {
319 319 let mut lines: Vec<String> = Vec::new();
320 320 let mut cur = String::new();
321 321 for word in text.split_whitespace() {
322 + // A single token wider than the column can never fit on any line; break it
323 + // on character boundaries so it wraps instead of running off the page edge.
324 + if text_width_mm(word, style.size_pt, style.bold, style.mono) > max_width_mm {
325 + if !cur.is_empty() {
326 + lines.push(std::mem::take(&mut cur));
327 + }
328 + let mut pieces = hard_break(word, style, max_width_mm);
329 + // Keep the trailing piece in `cur` so the next word can share its line.
330 + if let Some(last) = pieces.pop() {
331 + lines.append(&mut pieces);
332 + cur = last;
333 + }
334 + continue;
335 + }
322 336 let candidate = if cur.is_empty() {
323 337 word.to_string()
324 338 } else {
@@ -342,6 +356,28 @@ fn wrap(text: &str, style: Style, max_width_mm: f32) -> Vec<String> {
342 356 lines
343 357 }
344 358
359 + /// Break a single token wider than the column into character-boundary pieces that
360 + /// each fit within `max_width_mm`. A char wider than the column on its own still
361 + /// gets its own piece (nothing narrower exists), so a piece is never empty.
362 + fn hard_break(word: &str, style: Style, max_width_mm: f32) -> Vec<String> {
363 + let mut pieces = Vec::new();
364 + let mut cur = String::new();
365 + for ch in word.chars() {
366 + cur.push(ch);
367 + if text_width_mm(&cur, style.size_pt, style.bold, style.mono) > max_width_mm
368 + && cur.chars().count() > 1
369 + {
370 + cur.pop();
371 + pieces.push(std::mem::take(&mut cur));
372 + cur.push(ch);
373 + }
374 + }
375 + if !cur.is_empty() {
376 + pieces.push(cur);
377 + }
378 + pieces
379 + }
380 +
345 381 #[cfg(test)]
346 382 mod tests {
347 383 use super::*;
@@ -363,6 +399,28 @@ mod tests {
363 399 }
364 400
365 401 #[test]
402 + fn wrap_hard_breaks_overlong_token() {
403 + // A single token far wider than the column must be split, and no resulting
404 + // line may exceed the column width.
405 + let long = "supercalifragilisticexpialidocioussupercalifragilistic";
406 + let lines = wrap(long, Style::body(), 20.0);
407 + assert!(
408 + lines.len() > 1,
409 + "an over-wide token should wrap onto several lines"
410 + );
411 + let style = Style::body();
412 + for l in &lines {
413 + assert!(!l.is_empty());
414 + assert!(
415 + text_width_mm(l, style.size_pt, style.bold, style.mono) <= 20.0,
416 + "line {l:?} exceeds the column width",
417 + );
418 + }
419 + // No characters are lost across the break.
420 + assert_eq!(lines.concat(), long);
421 + }
422 +
423 + #[test]
366 424 fn empty_body_is_error() {
367 425 let out = render_article("<p></p>", "t", None);
368 426 assert!(matches!(out, Err(Error::Empty)));
@@ -370,12 +428,12 @@ mod tests {
370 428
371 429 #[test]
372 430 fn end_to_end_produces_pdf_bytes() {
373 - let html = r#"
431 + let html = r"
374 432 <h1>Hello</h1>
375 433 <p>This is a <strong>bold</strong> paragraph with an <em>italic</em> word.</p>
376 434 <ul><li>one</li><li>two</li></ul>
377 435 <blockquote>Quoted material.</blockquote>
378 - "#;
436 + ";
379 437 let bytes = render_article(html, "Test Article", Some("https://example.com/a")).unwrap();
380 438 assert!(bytes.starts_with(b"%PDF"));
381 439 assert!(bytes.len() > 1000);
@@ -57,6 +57,14 @@
57 57 }
58 58 }
59 59
60 + /** True if a resource URL uses a dangerous scheme or points off-origin. */
61 + function isBlockedResourceUrl(url) {
62 + const value = url.replace(/[\s\x00-\x1f]/g, '').toLowerCase();
63 + return value.startsWith('javascript:') || value.startsWith('data:') ||
64 + value.startsWith('vbscript:') || value.startsWith('http:') ||
65 + value.startsWith('https:') || value.startsWith('//');
66 + }
67 +
60 68 /** Remove dangerous attributes from a single element. */
61 69 function cleanAttributes(el) {
62 70 // Collect names first; removing during iteration can skip entries.
@@ -80,13 +88,17 @@
80 88 // render with no user action, so a remote URL here beacons the
81 89 // reader's IP to the feed publisher (tracking pixel). Strip both
82 90 // dangerous schemes and any remote (http/https/protocol-relative)
83 - // target. Normalize away whitespace/control chars to defeat
84 - // obfuscation. Relative URLs resolve against the app origin and
85 - // don't leak, so they're left intact.
91 + // target. Relative URLs resolve against the app origin and don't
92 + // leak, so they're left intact.
86 93 if (lower === 'src' || lower === 'srcset' || lower === 'poster') {
87 - const value = (el.getAttribute(name) || '').replace(/[\s\x00-\x1f]/g, '').toLowerCase();
88 - if (value.startsWith('javascript:') || value.startsWith('data:') || value.startsWith('vbscript:') ||
89 - value.startsWith('http:') || value.startsWith('https:') || value.startsWith('//')) {
94 + const raw = el.getAttribute(name) || '';
95 + // srcset is a comma-separated candidate list ("url 1x, url 2x");
96 + // each candidate's URL must be checked on its own, or a remote
97 + // URL in any position but the first slips past a whole-value test.
98 + const urls = lower === 'srcset'
99 + ? raw.split(',').map(c => c.trim().split(/\s+/, 1)[0])
100 + : [raw];
101 + if (urls.some(isBlockedResourceUrl)) {
90 102 el.removeAttribute(name);
91 103 }
92 104 continue;