Skip to content

fix: avoid integer overflow when calculating text alignment padding#188

Closed
robn wants to merge 1 commit into
ccbrown:mainfrom
robn:text-align-overflow
Closed

fix: avoid integer overflow when calculating text alignment padding#188
robn wants to merge 1 commit into
ccbrown:mainfrom
robn:text-align-overflow

Conversation

@robn
Copy link
Copy Markdown

@robn robn commented Apr 14, 2026

When using TextAlign::Right or TextAlign::Center, if the available width is less than the text width, the padding calculation in Text::alignment_padding() goes negative, causing a panic.

This just clamps the subtract amount to the available width, so the result can't go below zero.

Testing and reproduction

I don't actually know enough to know how to write a reduced test case. In my own program, it happens deep inside a set of flex views. Something like:

            View(
                flex_direction: FlexDirection::Row,
                justify_content: JustifyContent::Stretch,
                gap: 1,
            ) {
                View(
                    flex_direction: FlexDirection::Column,
                ) {
                    ...
                }
                View(
                    flex_direction: FlexDirection::Column,
                    flex_grow: 1.0,
                ) {
                    ...
                }
                View(
                    flex_direction: FlexDirection::Column,
                    min_width: 9,
                ) {
                    Text(
                        content: &props.content,
                        align: TextAlign::Center,
                        wrap: TextWrap::NoWrap
                    )
                }
                View(
                    flex_direction: FlexDirection::Column,
                ) {
                    ...
                }
            }
        }

As best I can tell, in the initial layout the View wrapping the Text is given the minimum width of 9, which is fine because props.content is a string shorter than that. At some point, there is an update and props.content is longer than that. The layout however is already set, so the overflow.

With this patch applied, the padding becomes 0, and the text is drawn, overflowing its bounds, but not crashing.

Panic & backtrace
thread 'main' (2141347) panicked at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/components/text.rs:138:34:
attempt to subtract with overflow
stack backtrace:
   0: __rustc::rust_begin_unwind
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/std/src/panicking.rs:689:5
   1: core::panicking::panic_fmt
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/panicking.rs:80:14
   2: core::panicking::panic_const::panic_const_sub_overflow
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/panicking.rs:175:17
   3: iocraft::components::text::Text::alignment_padding
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/components/text.rs:138:34
   4: iocraft::components::text::Text::align::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/components/text.rs:152:33
   5: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &mut F>::call_once
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/ops/function.rs:310:21
   6: core::option::Option<T>::map
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/option.rs:1165:29
   7: <core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::next
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/iter/adapters/map.rs:107:26
   8: <alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/alloc/src/vec/spec_from_iter_nested.rs:24:41
   9: <alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/alloc/src/vec/spec_from_iter.rs:33:9
  10: <alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/alloc/src/vec/mod.rs:3801:9
  11: core::iter::traits::iterator::Iterator::collect
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/iter/traits/iterator.rs:2035:9
  12: iocraft::components::text::Text::align
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/components/text.rs:155:18
  13: <iocraft::components::text::Text as iocraft::component::Component>::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/components/text.rs:244:23
  14: <C as iocraft::component::AnyComponent>::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:123:9
  15: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:203:28
  16: iocraft::component::Components::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:252:31
  17: iocraft::render::ComponentDrawer::for_child_node_layout
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:292:9
  18: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:251:24
  19: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  20: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  21: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  22: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:249:27
  23: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  24: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  25: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  26: iocraft::component::Components::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:252:31
  27: iocraft::render::ComponentDrawer::for_child_node_layout
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:292:9
  28: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:251:24
  29: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  30: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  31: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  32: iocraft::component::Components::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:252:31
  33: iocraft::render::ComponentDrawer::for_child_node_layout
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:292:9
  34: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:251:24
  35: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  36: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  37: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  38: iocraft::component::Components::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:252:31
  39: iocraft::render::ComponentDrawer::for_child_node_layout
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:292:9
  40: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:251:24
  41: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  42: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  43: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  44: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:249:27
  45: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  46: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  47: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  48: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:249:27
  49: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  50: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  51: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  52: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:249:27
  53: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  54: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  55: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  56: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:249:27
  57: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  58: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  59: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  60: iocraft::component::Components::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:252:31
  61: iocraft::render::ComponentDrawer::for_child_node_layout
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:292:9
  62: iocraft::component::Components::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:251:24
  63: iocraft::component::InstantiatedComponent::draw::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:207:27
  64: iocraft::render::ComponentDrawer::with_clip_rect_for_children
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:306:13
  65: iocraft::component::InstantiatedComponent::draw
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/component.rs:206:16
  66: iocraft::render::Tree::render
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:456:29
  67: iocraft::render::Tree::terminal_render_loop::{{closure}}::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:470:35
  68: iocraft::terminal::Terminal::synchronized_update
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/terminal.rs:524:9
  69: iocraft::render::Tree::terminal_render_loop::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:469:18
  70: iocraft::render::terminal_render_loop::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/render.rs:515:37
  71: <iocraft::element::RenderLoopFuture<E> as core::future::future::Future>::poll
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iocraft-0.8.0/src/element.rs:521:41
  72: async_io::driver::block_on::{{closure}}
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/src/driver.rs:204:53
  73: std::thread::local::LocalKey<T>::try_with
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/std/src/thread/local.rs:513:12
  74: std::thread::local::LocalKey<T>::with
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/std/src/thread/local.rs:477:20
  75: async_io::driver::block_on
             at /home/robn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/src/driver.rs:180:11
  76: app::main
             at ./app/src/main.rs:412:5
  77: core::ops::function::FnOnce::call_once
             at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/ops/function.rs:250:5

Alternative solutions

Its possible that this is papering over problem in my program. Should I have done something to force a re-layout? Though I doubt a crash is the right thing there regardless!

There's also a world where a negative padding would be nice; consider:

content="the quick brown fox" (line_width=19)
width=10

  Left: |the quick | (padding=0)
 Right: | brown fox| (padding=(10-19)=-9)
Center: |quick brow| (padding=((10-19)/2)=-4)

I don't know if that would go against the CSS idea of text alignment. It definitely would be useful for something I'm doing elsewhere though, effectively "anchoring" a very long text line to the right instead of the left. As it is, I had to write a component to get the layout width and take a substring of the content string.

If the available width is less than the text width, the calculated
padding goes negative, causing a panic. Instead, check and clamp the
subtraction so it never goes below zero.

Signed-off-by: Rob Norris <robn@despairlabs.com>
@ccbrown ccbrown added the bug Something isn't working label Apr 15, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.71%. Comparing base (090d2bb) to head (be6ced2).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #188   +/-   ##
=======================================
  Coverage   87.71%   87.71%           
=======================================
  Files          35       35           
  Lines        5526     5526           
  Branches     5526     5526           
=======================================
  Hits         4847     4847           
  Misses        570      570           
  Partials      109      109           
Files with missing lines Coverage Δ
packages/iocraft/src/components/text.rs 98.94% <100.00%> (ø)

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ccbrown
Copy link
Copy Markdown
Owner

ccbrown commented Apr 15, 2026

Thanks for the PR! I'm a little stumped as to how this panic is possible. Your fix is reasonable, but I'm worried this is a sign of some other subtle issue in the library.

Is there any chance you could provide a full reproduction of the issue? If your code is in a public repo I'd love to take a look and try to figure out exactly what's happening.

@robn
Copy link
Copy Markdown
Author

robn commented Apr 17, 2026

Alright, try this:

$ git clone -b iocraft-text-panic-demo https://github.com/robn/veneer.git
$ cd veneer
$ env RUST_BACKTRACE=1 cargo run -p dash 2> out

Panic should be in out.

There's three commits on there beyond current main. One pulls iocraft back to 0.8, the next hacks in some canned data so you don't have to have the right shape of OpenZFS pools running on your machine (!!), the third shows you the two numbers of interest.

My gut feeling (without much to back it up) is that its a side-effect of the View wrapping the Sparkline component (immediately to the left of the Meter that is being overrun). That View is flex_grow: 1.0, and the parent is justify_content: Stretch, so the Sparkline gets all the extra space. Shouldn't affect the View->Meter next to it, it gets its min_width: 9 (as evidenced by my alignment "fix", which makes the padding 0, and then the length=10 text draws outside the containing element).

Also note that if you change the min_width to 2, where its smaller than the original value for the Meter text (0 B ie length=3), it renders "correctly" the first time (ie draws outside the box); its not until the update that it fails. So presumably the length change is not being fully propagated somewhere, or something isn't clamped in the update path, only in the init path?

(I mean, kinda "duh", and I'll leave you to it. I'm just curious and spitballing a bit!)

Lemme know if I can help with more!

@robn
Copy link
Copy Markdown
Author

robn commented Apr 17, 2026

Incidentally, Sparkline is the place where a right "anchor" would have been nice. It had an earlier version that was pretty much just a Text, with a 256-char content string. If I could pin it to the right, then I wouldn't need to know the width of the area, and so wouldn't need the custom component. I can make a separate feature request if you like, but I'm not so bothered really - it was interesting to learn how to build a component without the macros, and we got that MeasureFunc fix out of it, so its paid off in lots of ways already!

@ccbrown
Copy link
Copy Markdown
Owner

ccbrown commented Apr 18, 2026

Thank you so much for providing that code. Using it as a starting point, I managed to boil this down to a very simple minimal reproduction:

element! {
    View(
        flex_direction: FlexDirection::Column,
        width: 9,
    ) {
        Text(
            content: "123123123123",
            align: TextAlign::Center,
            wrap: TextWrap::NoWrap
        )
    }
}
.print();

Your fix here would have prevented the panic, but the alignment would have still been incorrect. In cases like this, the text should be shifted over to the left (effectively negative alignment padding). I reworked the alignment logic in Text and MixedText to allow for this and eliminate the panic in #193, which I've gone ahead and merged.

Thanks again for the help!

@ccbrown ccbrown closed this Apr 18, 2026
@robn
Copy link
Copy Markdown
Author

robn commented Apr 18, 2026

Ahh, very nice! I'm glad it was such a simple repro. Looking forward to trying it soon! Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants