Repository: jugglerchris/rust-html2text (rust)

Issue:
certain pages cause memory allocation failure
version: 0.12.5
a sample url to reproduce issue: https://docs.stripe.com/refunds
simply call `html2text::from_read_with_decorator(cursor, 100, TrivialDecorator::new());` cause a `memory allocation of 216172782113783784 bytes failed` error and crashes the program on MacOS

Pull request:
Fix #171
We were allocating a vector sized for the total number of table columns, taking account of colspan.  This goes wrong when there's a ridiculous colspan (an example was ~1<<53).  Instead count the number of column indices where a cell starts, and renumber everything to collapse duplicate spans.

Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 515728c..e88397f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -81,6 +81,7 @@ use markup5ever_rcdom::{
 };
 use std::cell::Cell;
 use std::cmp::{max, min};
+use std::collections::{BTreeSet, HashMap};
 use unicode_width::UnicodeWidthStr;
 
 use std::io;
@@ -268,7 +269,52 @@ struct RenderTable {
 
 impl RenderTable {
     /// Create a new RenderTable with the given rows
-    fn new(rows: Vec<RenderTableRow>) -> RenderTable {
+    fn new(mut rows: Vec<RenderTableRow>) -> RenderTable {
+        // We later on want to allocate a vector sized by the column count,
+        // but occasionally we see something like colspan="1000000000".  We
+        // handle this by remapping the column ids to the smallest values
+        // possible.
+        //
+        // Tables with no explicit colspan will be unchanged, but if there
+        // are multiple columns each covered by a single <td> on every row,
+        // they will be collapsed into a single column.  For example:
+        //
+        //    <td><td colspan=1000><td>
+        //    <td colspan=1000><td><td>
+        //
+        //  becomes the equivalent:
+        //    <td><td colspan=2><td>
+        //    <td colspan=2><td><td>
+
+        // This will include 0 and the index after the last colspan.
+        let mut col_positions = BTreeSet::new();
+        col_positions.insert(0);
+        for row in &rows {
+            let mut col = 0;
+            for cell in row.cells() {
+                col += cell.colspan;
+                col_positions.insert(col);
+            }
+        }
+
+        let colmap: HashMap<_, _> = col_positions
+            .into_iter()
+            .enumerate()
+            .map(|(i, pos)| (pos, i))
+            .collect();
+
+        for row in &mut rows {
+            let mut pos = 0;
+            let mut mapped_pos = 0;
+            for cell in row.cells_mut() {
+                let nextpos = pos + cell.colspan.max(1);
+                let next_mapped_pos = *colmap.get(&nextpos).unwrap();
+                cell.colspan = next_mapped_pos - mapped_pos;
+                pos = nextpos;
+                mapped_pos = next_mapped_pos;
+            }
+        }
+
         let num_columns = rows.iter().map(|r| r.num_cells()).max().unwrap_or(0);
         RenderTable {
             rows,
diff --git a/src/render/text_renderer.rs b/src/render/text_renderer.rs
index 74134a3..f4e0be2 100644
--- a/src/render/text_renderer.rs
+++ b/src/render/text_renderer.rs
@@ -903,14 +903,15 @@ pub(crate) struct SubRenderer<D: TextDecorator> {
     pre_depth: usize,
 }
 
-impl<D: TextDecorator + Debug> std::fmt::Debug for SubRenderer<D> {
+impl<D: TextDecorator> std::fmt::Debug for SubRenderer<D> {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
         f.debug_struct("SubRenderer")
             .field("width", &self.width)
             .field("lines", &self.lines)
-            .field("decorator", &self.decorator)
+            //.field("decorator", &self.decorator)
             .field("ann_stack", &self.ann_stack)
             .field("pre_depth", &self.pre_depth)
+            .field("wrapping", &self.wrapping)
             .finish()
     }
 }


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index d41dfec..bb5fe97 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -332,21 +332,46 @@ fn test_colspan_large() {
      </tr>
    </table>
 "##,
-        r#"────────────
-1
-////////////
-2
-////////////
-3
-────────────
-12
-////////////
-3
-────────────
-1
-////////////
-23
-────────────
+        // FIXME: The extra long line blow is not ideal
+        r#"─┬─┬─
+1│2│3
+─┴─┼─
+12 │3
+─┬─┴─
+1│23  
+─┴────
+"#,
+        12,
+    );
+}
+
+#[test]
+fn test_colspan_larger() {
+    test_html(
+        br##"
+   <table>
+     <tr>
+       <td colspan="50">1</td>
+       <td colspan="50">2</td>
+       <td colspan="50">3</td>
+     </tr>
+     <tr>
+       <td colspan="100">12</td>
+       <td colspan="50">3</td>
+     </tr>
+     <tr>
+       <td colspan="50">1</td>
+       <td colspan="100">23</td>
+     </tr>
+   </table>
+"##,
+        r#"─┬─┬─
+1│2│3
+─┴─┼─
+12 │3
+─┬─┴─
+1│23 
+─┴───
 "#,
         12,
     );
@@ -2020,6 +2045,24 @@ fn test_empty_table_in_list() {
     );
 }
 
+#[test]
+fn test_silly_colspan() {
+    test_html(
+        br#"
+  <table>
+    <tr>
+      <td colspan="9007199254740991">foo</td.
+    </tr>
+  </table>
+"#,
+        r#"───
+foo
+───
+"#,
+        80,
+    );
+}
+
 #[cfg(feature = "css")]
 mod css_tests {
     use super::{test_html_coloured, test_html_css, test_html_style};