From f59ffd814d2a29998a002cab84227b95d2fd6c7b Mon Sep 17 00:00:00 2001 From: Jared Dellitt Date: Wed, 18 Mar 2026 13:53:03 -0500 Subject: [PATCH 1/7] chore(PLA-2392): do not attempt inlining for very deeploy nested html strings --- native/css_inline_nif/src/lib.rs | 49 ++ test/css_inline_test.exs | 7 + test/fixtures/deeply_nested.html | 816 +++++++++++++++++++++++++++++++ 3 files changed, 872 insertions(+) create mode 100644 test/fixtures/deeply_nested.html diff --git a/native/css_inline_nif/src/lib.rs b/native/css_inline_nif/src/lib.rs index d3e7ca6..2b04aa6 100644 --- a/native/css_inline_nif/src/lib.rs +++ b/native/css_inline_nif/src/lib.rs @@ -1,6 +1,49 @@ use css_inline::CSSInliner; use rustler::{Error as RustlerError, NifStruct}; +mod atoms { + rustler::atoms! { + nesting_depth_exceeded, + } +} + +/// Legitimate email HTML rarely exceeds 30–50 levels. This limit guards +/// against pathological inputs that cause stack overflows in +/// html5ever/cssparser's recursive descent parser. +/// +/// Note: the scanner overcounts because void elements (`
`, ``, etc.) +/// increment depth but are never closed. Real nesting of ~50 may report ~80-100. +const MAX_NESTING_DEPTH: usize = 128; + +/// Returns `true` if the DOM nesting depth exceeds `limit` via a fast byte +/// scan, without parsing. Counts ` bool { + let mut depth: usize = 0; + let mut i = 0; + while i < html.len() { + if html[i] == b'<' { + if i + 1 < html.len() { + if html[i + 1] == b'/' { + // saturating_sub prevents underflow on malformed HTML with unmatched closing tags. + depth = depth.saturating_sub(1); + } else if html[i + 1] != b'!' { + depth += 1; + if depth > limit { + return true; + } + } + } + // Skip to end of tag to avoid counting '<' inside attribute values. + while i < html.len() && html[i] != b'>' { + i += 1; + } + } + i += 1; + } + false +} + /// Options for CSS inlining, mapped from Elixir struct. #[derive(Debug, NifStruct)] #[module = "CSSInline.Options"] @@ -33,6 +76,12 @@ struct Options { /// since BEAM-managed data must live on the BEAM heap. #[rustler::nif(schedule = "DirtyCpu")] fn inline_css(html: &str, opts: Options) -> Result, RustlerError> { + if exceeds_nesting_depth(html.as_bytes(), MAX_NESTING_DEPTH) { + return Err(RustlerError::Term(Box::new( + atoms::nesting_depth_exceeded(), + ))); + } + let estimated_size = (html.len() as f64 * 1.5) as usize; let mut buffer: Vec = Vec::with_capacity(estimated_size); let inliner = CSSInliner::options() diff --git a/test/css_inline_test.exs b/test/css_inline_test.exs index 6b37863..10479aa 100644 --- a/test/css_inline_test.exs +++ b/test/css_inline_test.exs @@ -239,4 +239,11 @@ defmodule CSSInlineTest do end end end + + describe "regression tests" do + test "returns error for deeply nested HTML" do + html = File.read!("test/fixtures/deeply_nested.html") + assert {:error, :nesting_depth_exceeded} = CSSInline.inline(html) + end + end end diff --git a/test/fixtures/deeply_nested.html b/test/fixtures/deeply_nested.html new file mode 100644 index 0000000..836e865 --- /dev/null +++ b/test/fixtures/deeply_nested.html @@ -0,0 +1,816 @@ + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Hello +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file From 28bf211135162c71d4e75581cfc8a33ac95d4dd7 Mon Sep 17 00:00:00 2001 From: Jared Dellitt Date: Wed, 18 Mar 2026 14:01:52 -0500 Subject: [PATCH 2/7] chore(PLA-2392): format & bump css-inline version --- native/css_inline_nif/Cargo.lock | 185 +++++++++++++++---------------- native/css_inline_nif/Cargo.toml | 2 +- native/css_inline_nif/src/lib.rs | 6 +- 3 files changed, 96 insertions(+), 97 deletions(-) diff --git a/native/css_inline_nif/Cargo.lock b/native/css_inline_nif/Cargo.lock index 4132c7f..7110ef5 100644 --- a/native/css_inline_nif/Cargo.lock +++ b/native/css_inline_nif/Cargo.lock @@ -22,27 +22,27 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -87,13 +87,14 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "css-inline" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff246a3af11c7b3747663a5d96c991425ee75abc60ef2cbf32d61d1762196c12" +checksum = "cd805d099f55672ddb5caeb96375dbcc647919c73617073ba96951f6a9d30a94" dependencies = [ "cssparser", "html5ever", "lru", + "memchr", "precomputed-hash", "rayon", "reqwest", @@ -201,9 +202,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "foldhash" @@ -222,9 +223,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -232,33 +233,33 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -266,7 +267,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -403,14 +403,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -529,18 +528,18 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" dependencies = [ "rustversion", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -560,9 +559,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -570,9 +569,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -633,9 +632,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" @@ -656,9 +655,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot" @@ -744,9 +743,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -809,9 +808,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -844,9 +843,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -917,9 +916,9 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "reqwest" @@ -992,9 +991,9 @@ dependencies = [ [[package]] name = "rustler" -version = "0.37.2" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5c708d8b686a8d426681908369f835af90349f7ebb92ab87ddf14a851efd556" +checksum = "c779e2cbfa2987990205d0d8fc142163739e45a4c6592dc637896c77fec01280" dependencies = [ "inventory", "libloading", @@ -1004,9 +1003,9 @@ dependencies = [ [[package]] name = "rustler_codegen" -version = "0.37.2" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3f478ec72581782a7dd62a5adb406aa076af7cedd7de63fa3676c927eb216a" +checksum = "e6e120f8936c779b6c2e09992a2dfa9a4e8bcd0794c02bb654fde03e03ce8c31" dependencies = [ "heck", "inventory", @@ -1017,9 +1016,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -1058,9 +1057,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" @@ -1165,15 +1164,15 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1183,12 +1182,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1229,9 +1228,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1300,9 +1299,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -1315,9 +1314,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -1409,9 +1408,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "untrusted" @@ -1469,9 +1468,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1482,9 +1481,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -1496,9 +1495,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1506,9 +1505,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1519,18 +1518,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -1548,9 +1547,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fa72497c57079de16225d9a886d6c9a80c34f8e5a9cd5c64b71a449cbba195" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" dependencies = [ "phf", "phf_codegen", @@ -1560,9 +1559,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -1766,18 +1765,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", @@ -1846,6 +1845,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/native/css_inline_nif/Cargo.toml b/native/css_inline_nif/Cargo.toml index ade25c5..4598c49 100644 --- a/native/css_inline_nif/Cargo.toml +++ b/native/css_inline_nif/Cargo.toml @@ -17,4 +17,4 @@ nif_version_2_17 = ["rustler/nif_version_2_17"] [dependencies] rustler = "0.37" -css-inline = "0.19.1" +css-inline = "0.20.0" diff --git a/native/css_inline_nif/src/lib.rs b/native/css_inline_nif/src/lib.rs index 2b04aa6..801c5b8 100644 --- a/native/css_inline_nif/src/lib.rs +++ b/native/css_inline_nif/src/lib.rs @@ -77,9 +77,9 @@ struct Options { #[rustler::nif(schedule = "DirtyCpu")] fn inline_css(html: &str, opts: Options) -> Result, RustlerError> { if exceeds_nesting_depth(html.as_bytes(), MAX_NESTING_DEPTH) { - return Err(RustlerError::Term(Box::new( - atoms::nesting_depth_exceeded(), - ))); + return Err(RustlerError::Term( + Box::new(atoms::nesting_depth_exceeded()), + )); } let estimated_size = (html.len() as f64 * 1.5) as usize; From c235b94c58ddfae21282928be1c5943bd2ca8fa8 Mon Sep 17 00:00:00 2001 From: Jared Dellitt Date: Wed, 18 Mar 2026 14:06:45 -0500 Subject: [PATCH 3/7] chore(PLA-2392): bump version & update rustler --- mix.exs | 2 +- mix.lock | 2 +- native/css_inline_nif/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index 88e035d..24b6f75 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule CSSInline.MixProject do use Mix.Project - @version "0.1.2" + @version "0.2.0" def project do [ diff --git a/mix.lock b/mix.lock index e1ca68f..4255fd7 100644 --- a/mix.lock +++ b/mix.lock @@ -7,6 +7,6 @@ "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, - "rustler": {:hex, :rustler, "0.37.1", "721434020c7f6f8e1cdc57f44f75c490435b01de96384f8ccb96043f12e8a7e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24547e9b8640cf00e6a2071acb710f3e12ce0346692e45098d84d45cdb54fd79"}, + "rustler": {:hex, :rustler, "0.37.3", "5f4e6634d43b26f0a69834dd1d3ed4e1710b022a053bf4a670220c9540c92602", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a6872c6f53dcf00486d1e7f9e046e20e01bf1654bdacc4193016c2e8002b32a2"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"}, } diff --git a/native/css_inline_nif/Cargo.toml b/native/css_inline_nif/Cargo.toml index 4598c49..1e4ea0c 100644 --- a/native/css_inline_nif/Cargo.toml +++ b/native/css_inline_nif/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "css_inline_nif" -version = "0.1.2" +version = "0.2.0" edition = "2021" authors = ["Knock Labs"] publish = false From 47a53bdfa9f2c413b64f76c9bd3ce9772212f796 Mon Sep 17 00:00:00 2001 From: Jared Dellitt Date: Wed, 18 Mar 2026 14:31:48 -0500 Subject: [PATCH 4/7] chore(PLA-2392): add max_depth as an option that can be passed to the inliner --- lib/css_inline.ex | 11 +++++++++-- native/css_inline_nif/src/lib.rs | 8 +++++++- test/css_inline_test.exs | 9 +++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/css_inline.ex b/lib/css_inline.ex index 69aecef..5dfca27 100644 --- a/lib/css_inline.ex +++ b/lib/css_inline.ex @@ -31,6 +31,8 @@ defmodule CSSInline do * `:load_remote_stylesheets` - Whether to load remote stylesheets referenced in `` tags. Defaults to `true`. Set to `false` to skip external stylesheets. * `:minify_css` - Whether to minify the inlined CSS. Defaults to `true`. + * `:max_depth` - Maximum allowed HTML nesting depth. Documents exceeding this return + `{:error, :nesting_depth_exceeded}`. Defaults to `128`. ## Performance @@ -47,14 +49,16 @@ defmodule CSSInline do keep_style_tags: false, keep_link_tags: false, load_remote_stylesheets: true, - minify_css: true + minify_css: true, + max_depth: 128 @type t :: %__MODULE__{ inline_style_tags: boolean(), keep_style_tags: boolean(), keep_link_tags: boolean(), load_remote_stylesheets: boolean(), - minify_css: boolean() + minify_css: boolean(), + max_depth: pos_integer() } end @@ -64,6 +68,7 @@ defmodule CSSInline do | {:keep_link_tags, boolean()} | {:load_remote_stylesheets, boolean()} | {:minify_css, boolean()} + | {:max_depth, pos_integer()} @doc """ Inlines CSS from `#{divs}

ok

#{closing}" + + assert {:ok, result} = CSSInline.inline(html) + assert result =~ "ok" + end end end From f7be4423ebb74af37988529393a09704b0428fcf Mon Sep 17 00:00:00 2001 From: Jared Dellitt Date: Wed, 18 Mar 2026 14:42:00 -0500 Subject: [PATCH 5/7] chore(PLA-2392): formatting --- test/css_inline_test.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/css_inline_test.exs b/test/css_inline_test.exs index 7a5497b..6beb20b 100644 --- a/test/css_inline_test.exs +++ b/test/css_inline_test.exs @@ -249,7 +249,9 @@ defmodule CSSInlineTest do test "accepts HTML near but under the nesting limit" do divs = String.duplicate("
", 100) closing = String.duplicate("
", 100) - html = "#{divs}

ok

#{closing}" + + html = + "#{divs}

ok

#{closing}" assert {:ok, result} = CSSInline.inline(html) assert result =~ "ok" From c8ae5636852fbe9ece77949a07510101317cbb2a Mon Sep 17 00:00:00 2001 From: Jared Dellitt Date: Wed, 18 Mar 2026 14:57:47 -0500 Subject: [PATCH 6/7] chore(PLA-2392): if max_depth is 0, disable depth checking --- lib/css_inline.ex | 10 +++++----- native/css_inline_nif/src/lib.rs | 7 +------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/css_inline.ex b/lib/css_inline.ex index 5dfca27..9b104a3 100644 --- a/lib/css_inline.ex +++ b/lib/css_inline.ex @@ -32,7 +32,7 @@ defmodule CSSInline do Defaults to `true`. Set to `false` to skip external stylesheets. * `:minify_css` - Whether to minify the inlined CSS. Defaults to `true`. * `:max_depth` - Maximum allowed HTML nesting depth. Documents exceeding this return - `{:error, :nesting_depth_exceeded}`. Defaults to `128`. + `{:error, :nesting_depth_exceeded}`. Set to `0` to disable depth checking. Defaults to `128`. ## Performance @@ -58,7 +58,7 @@ defmodule CSSInline do keep_link_tags: boolean(), load_remote_stylesheets: boolean(), minify_css: boolean(), - max_depth: pos_integer() + max_depth: non_neg_integer() } end @@ -68,7 +68,7 @@ defmodule CSSInline do | {:keep_link_tags, boolean()} | {:load_remote_stylesheets, boolean()} | {:minify_css, boolean()} - | {:max_depth, pos_integer()} + | {:max_depth, non_neg_integer()} @doc """ Inlines CSS from `