Repository: cobalt-org/liquid-rust (rust)

Issue:
parser: byte index 1 is not a char boundary
While fuzz testing parser, encountered following panic:

```rust
#[test]
fn test_fuzz() {
    if let Ok(_) = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse("˄{%")
    {}
}
```

```sh
thread 'test_fuzz' panicked at crates/core/src/parser/parser.rs:659:70:
byte index 1 is not a char boundary; it is inside '˄' (bytes 0..2) of `˄{%`
stack backtrace:
```

Panicked at:
https://github.com/cobalt-org/liquid-rust/blob/8de2d34629fd387295312f2d625556ac40921bcc/crates/core/src/parser/parser.rs#L659

Crash input: 
[crash-2dbc79c17b5c840978e94df8716233b7203e1e08.txt](https://github.com/user-attachments/files/16403759/crash-2dbc79c17b5c840978e94df8716233b7203e1e08.txt)



liquid-rust version: 0.26.8
rust version: 1.78.0
OS: Ubuntu 20.04.6 LTS


Pull request description:
fix(parser): Correctly bind 'char' boundaries
Fixes #553

Patch:
diff --git a/crates/core/src/parser/parser.rs b/crates/core/src/parser/parser.rs
index 92f7ddb..a1dbed6 100644
--- a/crates/core/src/parser/parser.rs
+++ b/crates/core/src/parser/parser.rs
@@ -649,7 +649,10 @@ impl<'a> InvalidLiquidToken<'a> {
         let invalid_token_position = invalid_token_span.start_pos();
         let (offset_l, offset_c) = invalid_token_position.line_col();
         let offset_l = offset_l - 1;
-        let offset_c = offset_c - 1;
+        let offset_c = (0..offset_c)
+            .rev()
+            .find(|i| invalid_token_position.line_of().is_char_boundary(*i))
+            .unwrap_or(0);
 
         let end_position = match next_elements.last() {
             Some(element) => element.as_span().end_pos(),


Test patch:
diff --git a/tests/errors.rs b/tests/errors.rs
new file mode 100644
index 0000000..86c8a13
--- /dev/null
+++ b/tests/errors.rs
@@ -0,0 +1,25 @@
+use snapbox::assert_data_eq;
+use snapbox::str;
+
+#[test]
+fn test_fuzz() {
+    match liquid::ParserBuilder::with_stdlib()
+        .build()
+        .unwrap()
+        .parse("˄{%")
+    {
+        Ok(_) => panic!("should fail"),
+        Err(err) => assert_data_eq!(
+            err.to_string(),
+            str![[r#"
+liquid:  --> 1:3
+  |
+1 | {%
+  |   ^---
+  |
+  = expected Identifier
+
+"#]]
+        ),
+    }
+}