From e3b9e239c52c14677be462e94d8920ea243de4f3 Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:20:32 -0400 Subject: [PATCH 1/7] docking: a window-edge drop beside an iconic column highlights only the edge (#336) A group dragged to the window edge where an iconic column sits showed the edge line and an outline around the column. The edge line is drawn just inside the window, so it lay in the column's body, and paint_drop_line outlines the iconic column a line lies in. That outline is for drops into the column; a window-edge drop docks beside it, so it now shows the line alone. --- crates/ui/src/docking/iconic_tests.rs | 21 +++++++++++++++++++++ crates/ui/src/docking/render/dock.rs | 10 +++++++--- crates/ui/src/docking/render/preview.rs | 6 +++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/ui/src/docking/iconic_tests.rs b/crates/ui/src/docking/iconic_tests.rs index 58a85fa5..0a353f2c 100644 --- a/crates/ui/src/docking/iconic_tests.rs +++ b/crates/ui/src/docking/iconic_tests.rs @@ -273,6 +273,27 @@ fn group_dropped_on_an_iconic_column_joins_as_an_icon_group() { assert!(first_column(&d.layout).iconic); } +/// A group dropped on the window edge beside an iconic column at that edge docks in a new column +/// there: the preview is the edge line alone, without the column's outline. +#[test] +fn an_edge_drop_next_to_an_iconic_column_highlights_only_the_edge() { + let mut layout = docked(&[&[1], &[2]], true); + layout.floating.push(Floating::single(vec![5, 6], 0, [300.0, 200.0, 240.0, 200.0])); + let mut d = DockDriver::new(layout, Content); + let bar = hit(&d, Hit::DragBar(5)).center(); + let target = pos2(2.0, 300.0); + d.hold(bar, target); + let column = Rect::from_min_max(pos2(0.0, 14.0), pos2(36.0, 600.0)); + assert!(d.filled(Rect::from_x_y_ranges(0.0..=4.0, 0.0..=600.0)), "the edge line"); + assert!(!d.outlined(column.shrink(1.0)), "the iconic column is not outlined"); + d.release(target); + let Some(Node::Split { first, .. }) = &d.layout.root else { panic!("{:?}", d.layout.root) }; + let Node::Dock { columns, .. } = first.as_ref() else { panic!("{first:?}") }; + let panels: Vec> = columns.iter().map(|c| c.groups.iter().flat_map(|g| g.panels.clone()).collect()).collect(); + assert_eq!(panels, vec![vec![5, 6], vec![1, 2]]); + assert!(!columns[0].iconic && columns[1].iconic); +} + /// Every kind of gesture source has a rect, and dragging from it does something. #[test] fn hit_rects_cover_every_gesture_source() { diff --git a/crates/ui/src/docking/render/dock.rs b/crates/ui/src/docking/render/dock.rs index 4eaab778..878d909e 100644 --- a/crates/ui/src/docking/render/dock.rs +++ b/crates/ui/src/docking/render/dock.rs @@ -789,11 +789,15 @@ impl Renderer<'_, P> { } /// The line of a drop that shows no result rect: on the window edge, into an iconic column, - /// or any dock drop with [`DockStyle::side_edge_zones`]. - pub(super) fn paint_drop_line(&self, ctx: &Context, line: Rect) { + /// or any dock drop with [`DockStyle::side_edge_zones`]. A line inside an iconic column also + /// outlines the column, unless it is on the window edge (`outer`): a column at that edge holds + /// the line, but the drop docks beside the column, not into it. + pub(super) fn paint_drop_line(&self, ctx: &Context, line: Rect, outer: bool) { if usable(line) { let painter = self.preview_painter(ctx); - self.iconic.outline(&painter, line, self.style); + if !outer { + self.iconic.outline(&painter, line, self.style); + } painter.rect_filled(line, 1.0, self.style.accent); } } diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs index 65c15fd3..5404984e 100644 --- a/crates/ui/src/docking/render/preview.rs +++ b/crates/ui/src/docking/render/preview.rs @@ -599,12 +599,12 @@ impl Renderer<'_, P> { if self.style.side_edge_zones { drop_preview(painter, proposal.preview.shrink(1.0), self.style.accent, filled); } else if matches!(proposal.placement, Placement::Edge(_)) { - self.paint_drop_line(ctx, proposal.preview); + self.paint_drop_line(ctx, proposal.preview, true); } else if beside { let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() }; match self.result_rect(action(to), members) { Some(rect) => tab_outline(painter, rect.shrink(1.0), self.style.accent), - None => self.paint_drop_line(ctx, proposal.preview), + None => self.paint_drop_line(ctx, proposal.preview, false), } } else { tab_outline(painter, proposal.preview.shrink(1.0), self.style.accent); @@ -621,7 +621,7 @@ impl Renderer<'_, P> { let rect = (!self.style.side_edge_zones && !drop.outer).then_some(action).and_then(|action| self.result_rect(action, members)); match rect { Some(rect) => tab_outline(painter, rect.shrink(1.0), self.style.accent), - None => self.paint_drop_line(ctx, drop.line), + None => self.paint_drop_line(ctx, drop.line, drop.outer), } } From 4f6fe25d77d1d4ba102f2821099824fca4619ac1 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 16:31:12 -0400 Subject: [PATCH 2/7] fix(update): draw the prompt's close mark and version pill without glyphs a font may lack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The close button was a ✕ (U+2715) text glyph and the version pill used →, neither of which the bundled faces are guaranteed to have; apps that panic on a missing glyph took the whole prompt down. The close mark is two painted strokes, the pill reads 'v0.1.0 to 0.2.0', and a test asserts the prompt paints only ASCII text. Fixes #343 --- crates/update/src/dialog.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/update/src/dialog.rs b/crates/update/src/dialog.rs index e43c38ae..15beefe7 100644 --- a/crates/update/src/dialog.rs +++ b/crates/update/src/dialog.rs @@ -91,13 +91,22 @@ impl StartupPrompt { ui.label(RichText::new(self.app.name).size(15.0).strong().color(text_primary)); ui.label(RichText::new("UPDATE").size(10.0).monospace().color(accent_color)); }); - let version_pill = format!("v{} → {}", self.running_version, self.release.version_label()); + // ASCII only, like the rest of the card: an app whose fonts lack a glyph may panic + // instead of drawing a box, and this prompt shows up unasked. + let version_pill = format!("v{} to {}", self.running_version, self.release.version_label()); ui.label(RichText::new(version_pill).size(11.0).monospace().color(text_muted)); }); ui.with_layout(Layout::right_to_left(Align::Min), |ui| { - let x_btn = egui::Button::new(RichText::new("✕").size(12.0).color(text_muted)).frame(false); - if ui.add(x_btn).clicked() { + // Two strokes, not a "✕" glyph: none of the bundled faces has U+2715. + let (rect, close) = ui.allocate_exact_size(Vec2::splat(18.0), egui::Sense::click()); + close.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Close")); + let color = if close.hovered() { text_primary } else { text_muted }; + let (center, reach) = (rect.center(), 4.5); + let stroke = egui::Stroke::new(1.5, color); + ui.painter().line_segment([center + Vec2::new(-reach, -reach), center + Vec2::new(reach, reach)], stroke); + ui.painter().line_segment([center + Vec2::new(-reach, reach), center + Vec2::new(reach, -reach)], stroke); + if close.on_hover_cursor(egui::CursorIcon::PointingHand).clicked() { response = StartupPromptResponse::Later; } }); @@ -304,7 +313,7 @@ mod tests { for expected in [ "PhotoCraft", "UPDATE", - "v0.1.0 → 0.2.0", + "v0.1.0 to 0.2.0", "PhotoCraft 0.2.0 is available with the latest features and bug fixes.", "What's new", "DOWNLOAD", @@ -313,6 +322,8 @@ mod tests { assert!(painted.contains(expected), "missing {expected:?} in painted text {painted:?}"); } assert!(!painted.contains("UPDATE AND RESTART"), "download branch must not offer the self-update button: {painted:?}"); + // No glyph that a font might lack: apps that panic on a missing glyph would take the prompt down (#343). + assert!(painted.is_ascii(), "the prompt paints a non-ASCII character: {painted:?}"); let fills = rect_fills(&output); let backdrop = fills.iter().any(|(rect, fill)| *fill == Color32::from_black_alpha(150) && rect.size() == screen().size()); assert!(backdrop, "modal backdrop missing: {fills:?}"); From 47edfe325591ff8bb31b654d33aed3cdcce879a7 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 16:31:38 -0400 Subject: [PATCH 3/7] fix(update): use egui::Role for the close button's accessibility info --- crates/update/src/dialog.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/update/src/dialog.rs b/crates/update/src/dialog.rs index 15beefe7..d6bf1dd4 100644 --- a/crates/update/src/dialog.rs +++ b/crates/update/src/dialog.rs @@ -100,7 +100,7 @@ impl StartupPrompt { ui.with_layout(Layout::right_to_left(Align::Min), |ui| { // Two strokes, not a "✕" glyph: none of the bundled faces has U+2715. let (rect, close) = ui.allocate_exact_size(Vec2::splat(18.0), egui::Sense::click()); - close.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Close")); + close.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, "Close")); let color = if close.hovered() { text_primary } else { text_muted }; let (center, reach) = (rect.center(), 4.5); let stroke = egui::Stroke::new(1.5, color); From ce020c5811d8621ddf26fc28d098bb5a8675636f Mon Sep 17 00:00:00 2001 From: skylar Date: Sun, 25 Oct 2026 15:45:08 -0600 Subject: [PATCH 4/7] chore(deps): bump roxmltree, miniz_oxide, serde_json, fixed_decimal craft-print: roxmltree 0.20 -> 0.21 (locked 0.21.2, fixes a stack overflow on deeply nested XML) and miniz_oxide 0.8 -> 0.9. Lockfile only: serde_json 1.0.154, fixed_decimal 0.7.3. --- Cargo.lock | 23 ++++++++++++++++------- crates/print/Cargo.toml | 8 ++++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70c29ac2..ee642d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,8 +899,8 @@ name = "craft-print" version = "0.1.0" dependencies = [ "craft-winprint-gdi", - "miniz_oxide 0.8.9", - "roxmltree", + "miniz_oxide 0.9.1", + "roxmltree 0.21.2", "tempfile", "winprint", "winsafe", @@ -1557,9 +1557,9 @@ checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "fixed_decimal" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd" +checksum = "2214fb00f669ee96bee5bb29dfb1ddabe058b2bc426817efc79afa03c9bc55ae" dependencies = [ "displaydoc", "smallvec", @@ -4160,6 +4160,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "roxmltree" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907" +dependencies = [ + "memchr", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4433,9 +4442,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -5537,7 +5546,7 @@ dependencies = [ "kurbo 0.11.3", "log", "pico-args", - "roxmltree", + "roxmltree 0.20.0", "simplecss", "siphasher", "strict-num", diff --git a/crates/print/Cargo.toml b/crates/print/Cargo.toml index 1f5aa1a8..1e6a6350 100644 --- a/crates/print/Cargo.toml +++ b/crates/print/Cargo.toml @@ -27,11 +27,11 @@ winsafe = { version = "0.0.29", default-features = false, features = ["winspool" # The job file's private folder. tempfile = "3" # Reading the PrintTicket and PrintCapabilities the driver hands back. -roxmltree = "0.20" +roxmltree = "0.21" # Inflating the print-ready PDF's cross-reference and object streams (src/sheets.rs). -miniz_oxide = "0.8" +miniz_oxide = "0.9" [dev-dependencies] tempfile = "3" -roxmltree = "0.20" -miniz_oxide = "0.8" +roxmltree = "0.21" +miniz_oxide = "0.9" From 8e42b9ff4e26cacd8a76a15b5379e330b6c8d77f Mon Sep 17 00:00:00 2001 From: skylar Date: Sun, 25 Oct 2026 16:52:57 -0600 Subject: [PATCH 5/7] fix(print): keep miniz_oxide at 0.8 --- Cargo.lock | 2 +- crates/print/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee642d1f..095fcc75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,7 +899,7 @@ name = "craft-print" version = "0.1.0" dependencies = [ "craft-winprint-gdi", - "miniz_oxide 0.9.1", + "miniz_oxide 0.8.9", "roxmltree 0.21.2", "tempfile", "winprint", diff --git a/crates/print/Cargo.toml b/crates/print/Cargo.toml index 1e6a6350..00ea396a 100644 --- a/crates/print/Cargo.toml +++ b/crates/print/Cargo.toml @@ -29,9 +29,9 @@ tempfile = "3" # Reading the PrintTicket and PrintCapabilities the driver hands back. roxmltree = "0.21" # Inflating the print-ready PDF's cross-reference and object streams (src/sheets.rs). -miniz_oxide = "0.9" +miniz_oxide = "0.8" [dev-dependencies] tempfile = "3" roxmltree = "0.21" -miniz_oxide = "0.9" +miniz_oxide = "0.8" From e3bcd5657fe59b15badd442d4b9b267878ee0c82 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 19:56:47 -0400 Subject: [PATCH 6/7] update(install): retry a busy disk-image attach for about a minute (#349) --- crates/update/src/install.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/update/src/install.rs b/crates/update/src/install.rs index c1433f18..09de4e0c 100644 --- a/crates/update/src/install.rs +++ b/crates/update/src/install.rs @@ -1307,8 +1307,9 @@ pub mod macos { Duration::from_secs(300), ) }; + // diskimages-helper can stay busy for a while on a shared runner; a minute of retries is still quick. let mut attached = attach(); - for wait in [1, 2, 4, 8] { + for wait in [1, 2, 4, 8, 16, 32] { if attached.is_ok() { break; } From cb532217676a6e7e5c9f7d865c6d348bb7ea3d86 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sun, 25 Oct 2026 18:37:49 -0600 Subject: [PATCH 7/7] fix(print): stop relying on inflate error output; bump miniz_oxide to 0.9 (#348) * fix(print): do not treat early inflate errors as partial output * chore(deps): bump miniz_oxide to 0.9 --- Cargo.lock | 2 +- crates/print/Cargo.toml | 4 +- crates/print/src/sheets.rs | 65 ++++++++++++++++++++++++---- crates/print/src/tests/pdf_bounds.rs | 58 +++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 095fcc75..ee642d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,7 +899,7 @@ name = "craft-print" version = "0.1.0" dependencies = [ "craft-winprint-gdi", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "roxmltree 0.21.2", "tempfile", "winprint", diff --git a/crates/print/Cargo.toml b/crates/print/Cargo.toml index 00ea396a..1e6a6350 100644 --- a/crates/print/Cargo.toml +++ b/crates/print/Cargo.toml @@ -29,9 +29,9 @@ tempfile = "3" # Reading the PrintTicket and PrintCapabilities the driver hands back. roxmltree = "0.21" # Inflating the print-ready PDF's cross-reference and object streams (src/sheets.rs). -miniz_oxide = "0.8" +miniz_oxide = "0.9" [dev-dependencies] tempfile = "3" roxmltree = "0.21" -miniz_oxide = "0.8" +miniz_oxide = "0.9" diff --git a/crates/print/src/sheets.rs b/crates/print/src/sheets.rs index 6e588f61..56303d2f 100644 --- a/crates/print/src/sheets.rs +++ b/crates/print/src/sheets.rs @@ -517,6 +517,53 @@ fn indirect<'a>(data: &'a [u8], offset: usize, budget: &mut Budget) -> Option), + /// The output would pass the limit. + Exhausted, + /// A damaged or truncated stream: only the bytes actually written before the damage. + Damaged(Vec), +} + +/// Inflate `raw` (zlib-wrapped or bare deflate) to at most `limit` bytes. Unlike the +/// `decompress_to_vec*` helpers it reports the bytes the decompressor really wrote, so a stream +/// rejected early yields an empty `Damaged`, never the zero-filled output buffer. +fn inflate(raw: &[u8], zlib: bool, limit: usize) -> Inflated { + use miniz_oxide::inflate::TINFLStatus; + use miniz_oxide::inflate::core::{DecompressorOxide, decompress, inflate_flags}; + let flags = inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF | if zlib { inflate_flags::TINFL_FLAG_PARSE_ZLIB_HEADER } else { 0 }; + let mut out = vec![0u8; raw.len().saturating_mul(2).max(64).min(limit)]; + let mut state = Box::::default(); + let (mut input, mut written) = (raw, 0usize); + loop { + let (status, consumed, wrote) = decompress(&mut state, input, &mut out, written, flags); + written = written.saturating_add(wrote).min(out.len()); + match status { + TINFLStatus::Done => { + out.truncate(written); + return Inflated::Done(out); + } + TINFLStatus::HasMoreOutput => { + if out.len() >= limit { + return Inflated::Exhausted; + } + let Some(rest) = input.get(consumed..) else { + out.truncate(written); + return Inflated::Damaged(out); + }; + input = rest; + out.resize(out.len().saturating_mul(2).min(limit), 0); + } + _ => { + out.truncate(written); + return Inflated::Damaged(out); + } + } + } +} + /// A stream's data with its filters undone: FlateDecode (with PNG predictors), or none. fn decode(dict: &Dict, raw: &[u8], budget: &mut Budget) -> Option> { if budget.exhausted { @@ -547,20 +594,22 @@ fn decode(dict: &Dict, raw: &[u8], budget: &mut Budget) -> Option> { Some(raw.to_vec()) } [b"FlateDecode" | b"Fl"] => { - let inflated = match miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(raw, limit) { - Ok(v) => v, - Err(e) if e.status == miniz_oxide::inflate::TINFLStatus::HasMoreOutput => { + let inflated = match inflate(raw, true, limit) { + Inflated::Done(v) => v, + Inflated::Exhausted => { budget.exhausted = true; return None; } // A damaged stream: what inflated before the damage, as other readers do. - Err(e) if !e.output.is_empty() => e.output, - Err(_) => match miniz_oxide::inflate::decompress_to_vec_with_limit(raw, limit) { - Ok(v) => v, - Err(e) => { - budget.exhausted |= e.status == miniz_oxide::inflate::TINFLStatus::HasMoreOutput; + Inflated::Damaged(v) if !v.is_empty() => v, + // Rejected before any output: retry as a bare deflate stream. + Inflated::Damaged(_) => match inflate(raw, false, limit) { + Inflated::Done(v) => v, + Inflated::Exhausted => { + budget.exhausted = true; return None; } + Inflated::Damaged(_) => return None, }, }; unpredict(&inflated, parms, limit) diff --git a/crates/print/src/tests/pdf_bounds.rs b/crates/print/src/tests/pdf_bounds.rs index e33922bf..b9bc15a2 100644 --- a/crates/print/src/tests/pdf_bounds.rs +++ b/crates/print/src/tests/pdf_bounds.rs @@ -418,3 +418,61 @@ fn large_inherited_resources_are_written_once_for_every_copy() { assert!(updated.len() < pdf.len() + (4 << 20), "{} bytes", updated.len()); assert_eq!(read(&updated).unwrap().sheets.len(), 3 * pages); } + +fn flate_dict() -> Dict { + vec![(b"Filter".to_vec(), Obj::Name(b"FlateDecode".to_vec()))] +} + +fn varied(len: usize) -> Vec { + (0..len).map(|i| u8::try_from(i * i % 251).unwrap_or(0)).collect() +} + +#[test] +fn a_valid_flate_stream_round_trips() { + let raw = varied(4000); + let packed = miniz_oxide::deflate::compress_to_vec_zlib(&raw, 6); + let mut budget = Budget::default(); + assert_eq!(decode(&flate_dict(), &packed, &mut budget), Some(raw.clone())); + assert_eq!(budget.decoded, raw.len()); + assert!(!budget.exhausted); +} + +#[test] +fn a_truncated_flate_stream_yields_only_the_bytes_it_inflated() { + let raw = varied(4000); + let packed = miniz_oxide::deflate::compress_to_vec_zlib(&raw, 6); + let cut = packed.get(..packed.len() * 3 / 4).unwrap_or_default(); + let mut budget = Budget::default(); + let got = decode(&flate_dict(), cut, &mut budget).unwrap_or_default(); + assert!(!got.is_empty() && got.len() < raw.len()); + assert!(raw.starts_with(&got), "a prefix of the data, with no zero padding"); + assert!(!budget.exhausted); +} + +#[test] +fn a_stream_rejected_before_any_output_is_none_not_zeros() { + // A zlib header, then a reserved deflate block type: nothing is ever inflated, and the + // bare-deflate retry rejects it too. + let bad = [0x78, 0x9c, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff]; + let mut budget = Budget::default(); + assert!(decode(&flate_dict(), &bad, &mut budget).is_none()); + assert_eq!(budget.decoded, 0); + assert!(!budget.exhausted); +} + +#[test] +fn a_bare_deflate_stream_still_decodes_through_the_retry() { + let raw = varied(500); + let packed = miniz_oxide::deflate::compress_to_vec(&raw, 6); + let mut budget = Budget::default(); + assert_eq!(decode(&flate_dict(), &packed, &mut budget), Some(raw)); +} + +#[test] +fn an_over_limit_flate_stream_sets_exhausted() { + let packed = miniz_oxide::deflate::compress_to_vec_zlib(&vec![7u8; 4096], 6); + let mut budget = Budget { decoded: MAX_DECODED - 100, ..Budget::default() }; + assert!(decode(&flate_dict(), &packed, &mut budget).is_none()); + assert!(budget.exhausted); + assert_eq!(budget.decoded, MAX_DECODED - 100); +}