Skip to content

Panic when putting multiple annotations on a snippet with a multibyte character and no EOL #271

@woodruffw

Description

@woodruffw

First, thanks a ton for creating and maintaining this library (and apologies for the mouthful of a issue title).

I'm reporting this based on user behavior observed in zizmorcore/zizmor#1065 -- we use annotate-snippets to render static analysis findings, and a user observed a crash on an input that appears to be well-formed and is only accessed via valid UTF-8 spans within the application code itself.

The conditions for the panic are pretty subtle:

  1. The snippet's source must contain multiple lines, but the final line shouldn't have a trailing linefeed;
  2. The snippet must have multiple annotations;
  3. The last character in the snippet should be multibyte (e.g. an emoji).

When put together, these conditions cause a panic when rendering multiple annotations that span to the end of the snippet's source. This panic doesn't happen when the snippet's source ends in a final linefeed.

Here's my attempt at a minified reproducer for the above, with the latest release of annotated-snippets (0.11.5):

use annotate_snippets::{Level, Renderer, Snippet};

fn main() {
    let good = r#"foobar

            foobar 🚀
"#;
    let snippet = Snippet::source(good)
        .fold(true)
        .line_start(1)
        .origin("whatever")
        .annotation(Level::Warning.span(0..good.len()).label("blah"))
        .annotation(Level::Warning.span(0..good.len()).label("blah"));
    let message = Level::Warning.title("whatever").snippet(snippet);

    let renderer = Renderer::styled();
    println!("{}", renderer.render(message));

    let bad = r#"foobar

            foobar 🚀"#;

    let snippet = Snippet::source(bad)
        .fold(true)
        .line_start(1)
        .origin("whatever")
        .annotation(Level::Warning.span(0..bad.len()).label("blah"))
        .annotation(Level::Warning.span(0..bad.len()).label("blah"));
    let message = Level::Warning.title("whatever").snippet(snippet);

    let renderer = Renderer::styled();
    println!("{}", renderer.render(message));
}

Observe that the only difference between good and bad is that good has a trailing linefeed, while bad does not. In both cases, the annotation spans are valid UTF-8 slices.

Here's what the panic looks like (after a successful render of good first):

$ ./target/debug/repro 
warning: whatever
 --> whatever:1:1
  |
1 | / foobar
2 | |
3 | |             foobar 🚀
  | |                      -
  | |______________________|
  |                        blah
  |                        blah
  |

thread 'main' panicked at /home/william/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/annotate-snippets-0.11.5/src/renderer/display_list.rs:1440:29:
byte index 22 is not a char boundary; it is inside '🚀' (bytes 19..23) of `            foobar 🚀`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Specifically, it looks like the panicking slice happens here:

[0..(start - line_start_index).min(line_length)]

It looks like this file no longer exists on main, so it's possible this bug has been addressed in a refactor that hasn't been put in a release yet. If so, I apologize for the noise!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions