Repository: djc/askama
Pull request #798 (2023-03-27 03:10:34)

Use `try_reserve()` instead of `with_capacity()`
`String::with_capacity()` panics if the requested memory could not be allocated. `Template::render()` is a fallible method, and the fact that it can panic is not documented.

This commit uses `String::try_reserve()` instead, so even for an exceedingly large `SIZE_HINT` the method should not panic. In the generated code, `write!()` calls may fail with `Err(std::fmt::Error)` instead.

I do not test if `try_reserve()` returned an error, because the rendering might succeed anyway, if less bytes are written than estimated.


Patch:
diff --git a/askama/Cargo.toml b/askama/Cargo.toml
index 62dfe4b..95554da 100644
--- a/askama/Cargo.toml
+++ b/askama/Cargo.toml
@@ -41,7 +41,7 @@ mime_guess = []
 [dependencies]
 askama_derive = { version = "0.12.0", path = "../askama_derive" }
 askama_escape = { version = "0.10.3", path = "../askama_escape" }
-comrak = { version = "0.16", optional = true, default-features = false }
+comrak = { version = "0.17", optional = true, default-features = false }
 dep_humansize = { package = "humansize", version = "2", optional = true }
 dep_num_traits = { package = "num-traits", version = "0.2.6", optional = true }
 percent-encoding = { version = "2.1.0", optional = true }
diff --git a/askama/src/filters/mod.rs b/askama/src/filters/mod.rs
index f76a463..642641a 100644
--- a/askama/src/filters/mod.rs
+++ b/askama/src/filters/mod.rs
@@ -377,12 +377,13 @@ where
             relaxed_tasklist_matching: false,
         },
         render: ComrakRenderOptions {
-            unsafe_: false,
             escape: true,
             // default:
             hardbreaks: false,
             github_pre_lang: false,
+            full_info_string: false,
             width: 0,
+            unsafe_: false,
             list_style: ListStyleType::Dash,
         },
     };
diff --git a/askama/src/lib.rs b/askama/src/lib.rs
index 17085b5..8806490 100644
--- a/askama/src/lib.rs
+++ b/askama/src/lib.rs
@@ -82,7 +82,8 @@ pub use crate::error::{Error, Result};
 pub trait Template: fmt::Display {
     /// Helper method which allocates a new `String` and renders into it
     fn render(&self) -> Result<String> {
-        let mut buf = String::with_capacity(Self::SIZE_HINT);
+        let mut buf = String::new();
+        let _ = buf.try_reserve(Self::SIZE_HINT);
         self.render_into(&mut buf)?;
         Ok(buf)
     }
@@ -99,7 +100,15 @@ pub trait Template: fmt::Display {
     /// The template's extension, if provided
     const EXTENSION: Option<&'static str>;
 
-    /// Provides a conservative estimate of the expanded length of the rendered template
+    /// Provides a rough estimate of the expanded length of the rendered template. Larger
+    /// values result in higher memory usage but fewer reallocations. Smaller values result in the
+    /// opposite. This value only affects [`render`]. It does not take effect when calling
+    /// [`render_into`], [`write_into`], the [`fmt::Display`] implementation, or the blanket
+    /// [`ToString::to_string`] implementation.
+    ///
+    /// [`render`]: Template::render
+    /// [`render_into`]: Template::render_into
+    /// [`write_into`]: Template::write_into
     const SIZE_HINT: usize;
 
     /// The MIME type (Content-Type) of the data that gets rendered by this Template
diff --git a/askama_escape/src/lib.rs b/askama_escape/src/lib.rs
index c388f2d..e09a32d 100644
--- a/askama_escape/src/lib.rs
+++ b/askama_escape/src/lib.rs
@@ -113,21 +113,26 @@ impl Escaper for Html {
     {
         let mut last = 0;
         for (index, byte) in string.bytes().enumerate() {
-            macro_rules! go {
-                ($expr:expr) => {{
-                    fmt.write_str(&string[last..index])?;
-                    fmt.write_str($expr)?;
-                    last = index + 1;
-                }};
-            }
-
-            match byte {
-                b'<' => go!("&lt;"),
-                b'>' => go!("&gt;"),
-                b'&' => go!("&amp;"),
-                b'"' => go!("&quot;"),
-                b'\'' => go!("&#x27;"),
-                _ => {}
+            const MIN_CHAR: u8 = b'"';
+            const MAX_CHAR: u8 = b'>';
+            const TABLE: [Option<&&str>; (MAX_CHAR - MIN_CHAR + 1) as usize] = {
+                let mut table = [None; (MAX_CHAR - MIN_CHAR + 1) as usize];
+                table[(b'<' - MIN_CHAR) as usize] = Some(&"&lt;");
+                table[(b'>' - MIN_CHAR) as usize] = Some(&"&gt;");
+                table[(b'&' - MIN_CHAR) as usize] = Some(&"&amp;");
+                table[(b'"' - MIN_CHAR) as usize] = Some(&"&quot;");
+                table[(b'\'' - MIN_CHAR) as usize] = Some(&"&#x27;");
+                table
+            };
+
+            let escaped = match byte {
+                MIN_CHAR..=MAX_CHAR => TABLE[(byte - MIN_CHAR) as usize],
+                _ => None,
+            };
+            if let Some(escaped) = escaped {
+                fmt.write_str(&string[last..index])?;
+                fmt.write_str(escaped)?;
+                last = index + 1;
             }
         }
         fmt.write_str(&string[last..])
@@ -181,20 +186,17 @@ impl std::io::Write for JsonEscapeBuffer {
     fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
         let mut last = 0;
         for (index, byte) in bytes.iter().enumerate() {
-            macro_rules! go {
-                ($expr:expr) => {{
-                    self.0.extend(&bytes[last..index]);
-                    self.0.extend($expr);
-                    last = index + 1;
-                }};
-            }
-
-            match byte {
-                b'&' => go!(br#"\u0026"#),
-                b'\'' => go!(br#"\u0027"#),
-                b'<' => go!(br#"\u003c"#),
-                b'>' => go!(br#"\u003e"#),
-                _ => {}
+            let escaped = match byte {
+                b'&' => Some(br#"\u0026"#),
+                b'\'' => Some(br#"\u0027"#),
+                b'<' => Some(br#"\u003c"#),
+                b'>' => Some(br#"\u003e"#),
+                _ => None,
+            };
+            if let Some(escaped) = escaped {
+                self.0.extend(&bytes[last..index]);
+                self.0.extend(escaped);
+                last = index + 1;
             }
         }
         self.0.extend(&bytes[last..]);


Test patch:
diff --git a/testing/Cargo.toml b/testing/Cargo.toml
index 8a68345..6e973a4 100644
--- a/testing/Cargo.toml
+++ b/testing/Cargo.toml
@@ -14,7 +14,7 @@ markdown = ["comrak", "askama/markdown"]
 
 [dependencies]
 askama = { path = "../askama", version = "0.12" }
-comrak = { version = "0.16", default-features = false, optional = true }
+comrak = { version = "0.17", default-features = false, optional = true }
 serde_json = { version = "1.0", optional = true }
 
 [dev-dependencies]