Merge PR #7143: fix(natives): prevent deep HTML crashes and silent truncation (@br411)
# Conflicts: # MODULE.bazel.lock
This commit is contained in:
Generated
+70
-13
@@ -444,6 +444,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
|
||||
|
||||
[[package]]
|
||||
name = "base64-simd"
|
||||
version = "0.8.0"
|
||||
@@ -2648,6 +2654,11 @@ name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -2710,17 +2721,20 @@ checksum = "46c1ff2d1cbf39efe5af0900ced8a069b5e61557a17544eb0c4a50239937389e"
|
||||
|
||||
[[package]]
|
||||
name = "html-to-markdown-rs"
|
||||
version = "2.30.0"
|
||||
version = "3.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ea41945a2fd834381642a000ef75b03f0030f3023f3dd3291fc5c372d3dda33"
|
||||
checksum = "ac9408651be7d7f6a6f51d7bebe5f30815ef2fe620314ad5713f989cee7d70d0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"astral-tl",
|
||||
"base64",
|
||||
"base64 0.23.0",
|
||||
"bitflags 2.13.1",
|
||||
"html-escape",
|
||||
"html5ever",
|
||||
"lru",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"phf 0.14.0",
|
||||
"regex",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
@@ -3339,7 +3353,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c264fe397c981705976c71f1bfe020382b9eda52ae950e57fe885e147bdd67d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"jaq-core",
|
||||
"libm",
|
||||
@@ -3621,11 +3635,11 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.16.4"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
|
||||
checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4549,7 +4563,7 @@ version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
@@ -4645,11 +4659,22 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_macros 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
|
||||
dependencies = [
|
||||
"phf_macros 0.14.0",
|
||||
"phf_shared 0.14.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
@@ -4690,6 +4715,16 @@ dependencies = [
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.13.1"
|
||||
@@ -4703,6 +4738,19 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085"
|
||||
dependencies = [
|
||||
"phf_generator 0.14.0",
|
||||
"phf_shared 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
@@ -4730,6 +4778,15 @@ dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pi-ast"
|
||||
version = "17.2.1"
|
||||
@@ -4818,7 +4875,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"ast-grep-core",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"clipboard-win",
|
||||
"core-graphics",
|
||||
@@ -6212,7 +6269,7 @@ version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e3b8d5ad1dd4c8cd0595440f26521a71dd593cb5873ee8e06b91510b7d97269"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"crc",
|
||||
"lazy_static",
|
||||
"md-5",
|
||||
@@ -6431,7 +6488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4a168cfc1d8ed65bf17a6ee0843ad9a68f863c63c0fb2fa7eab67838782ee"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bstr",
|
||||
"fancy-regex 0.17.0",
|
||||
"lazy_static",
|
||||
@@ -7247,7 +7304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d99249a493335eb44c4d7943a8751b22561a82c9903d8eb2d29780b3927880a7"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"futures",
|
||||
"log",
|
||||
"md-5",
|
||||
|
||||
+1
-1
@@ -310,7 +310,7 @@ syntect = { version = "5.3", default-features = false, features = [
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Markup Conversion
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
html-to-markdown-rs = { version = "2.24", default-features = false }
|
||||
html-to-markdown-rs = { version = "3.9.2", default-features = false }
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Tokenization
|
||||
|
||||
Generated
+82
-17
File diff suppressed because one or more lines are too long
@@ -1,6 +1,8 @@
|
||||
//! HTML to Markdown conversion.
|
||||
|
||||
use html_to_markdown_rs::{ConversionOptions, PreprocessingOptions, PreprocessingPreset, convert};
|
||||
use html_to_markdown_rs::{
|
||||
ConversionOptions, PreprocessingOptions, PreprocessingPreset, WarningKind, convert,
|
||||
};
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi_derive::napi;
|
||||
|
||||
@@ -41,7 +43,15 @@ pub fn html_to_markdown(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
convert(html.as_str(), Some(conversion_opts))
|
||||
.map_err(|err| Error::from_reason(format!("Conversion error: {err}")))
|
||||
let result = convert(html.as_str(), Some(conversion_opts))
|
||||
.map_err(|err| Error::from_reason(format!("Conversion error: {err}")))?;
|
||||
if let Some(warning) = result
|
||||
.warnings
|
||||
.iter()
|
||||
.find(|warning| warning.kind == WarningKind::DepthLimitExceeded)
|
||||
{
|
||||
return Err(Error::from_reason(format!("Conversion error: {}", warning.message)));
|
||||
}
|
||||
Ok(result.content.unwrap_or_default())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ Failure transitions:
|
||||
|
||||
1. `htmlToMarkdown(html, options)` schedules a blocking conversion task.
|
||||
2. Conversion runs with defaulted options (`cleanContent=false`, `skipImages=false`) unless specified.
|
||||
3. Returns markdown string or rejects with `Conversion error: ...`.
|
||||
3. The upstream converter owns normalization and preprocessing, makes affected auxiliary traversals iterative, and caps remaining recursive DOM traversal at 64; hitting that cap rejects the conversion instead of returning partial Markdown.
|
||||
4. Returns markdown string or rejects with `Conversion error: ...`.
|
||||
|
||||
### Clipboard lifecycle
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
|
||||
- Fixed `/live` corrupting the heap when opening PulseAudio on Linux ARM64 by shipping target-specific miniaudio Rust layouts for GNU and musl native addons ([#7138](https://github.com/can1357/oh-my-pi/pull/7138) by [@olegpulatov](https://github.com/olegpulatov)).
|
||||
- Fixed local Bazel addon builds on NixOS by exposing system CMake tools to sandboxed build scripts and forcing bundled Opus into the library directory expected by `audiopus_sys` ([#7136](https://github.com/can1357/oh-my-pi/pull/7136) by [@olegpulatov](https://github.com/olegpulatov)).
|
||||
### Changed
|
||||
|
||||
- Updated native HTML-to-Markdown rendering to html-to-markdown-rs 3.9.2 defaults; Markdown formatting can differ from 2.30.0, including fenced code blocks and cycling nested-list bullets.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed pathological HTML inputs crashing the process; conversions that reach the native-stack DOM depth limit now reject instead of returning silently truncated Markdown.
|
||||
|
||||
## [17.2.1] - 2026-07-30
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
wrapTextWithAnsi,
|
||||
} from "../native/index.js";
|
||||
|
||||
const addonUrl = new URL("../native/index.js", import.meta.url).href;
|
||||
|
||||
let testDir: string;
|
||||
|
||||
async function setupFixtures() {
|
||||
@@ -800,6 +802,86 @@ describe("pi-natives", () => {
|
||||
expect(cleaned).toContain("Main content");
|
||||
// Navigation/footer may or may not be removed depending on preprocessing
|
||||
});
|
||||
|
||||
it("should reject depth-truncated HTML", async () => {
|
||||
const html = `${"<div>".repeat(90)}<p>deep-content</p>${"</div>".repeat(90)}`;
|
||||
|
||||
await expect(htmlToMarkdown(html, { cleanContent: true })).rejects.toThrow(
|
||||
/Conversion error: .*effective depth limit of 64/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should survive pathologically deep HTML", async () => {
|
||||
const script = `
|
||||
import { htmlToMarkdown } from ${JSON.stringify(addonUrl)};
|
||||
|
||||
const cases = [
|
||||
{
|
||||
label: "balanced-div",
|
||||
input: "<div>".repeat(5_000) + "leaf" + "</div>".repeat(5_000),
|
||||
},
|
||||
{
|
||||
label: "malformed-table",
|
||||
input: "<table><tr>" + "<td>leaf".repeat(20_000),
|
||||
},
|
||||
];
|
||||
|
||||
for (const { label, input } of cases) {
|
||||
console.error("case=" + label + ":start");
|
||||
const pending = htmlToMarkdown(input, { cleanContent: true });
|
||||
if (pending === null || typeof pending.then !== "function") {
|
||||
throw new TypeError("htmlToMarkdown did not return a Promise for " + label);
|
||||
}
|
||||
|
||||
let rejected = false;
|
||||
let value;
|
||||
try {
|
||||
value = await pending;
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected && typeof value !== "string") {
|
||||
throw new TypeError("htmlToMarkdown fulfilled with a non-string for " + label);
|
||||
}
|
||||
console.error("case=" + label + ":done");
|
||||
}
|
||||
|
||||
console.log("ok");
|
||||
`;
|
||||
const child = Bun.spawn([process.execPath, "--eval", script], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const pid = child.pid;
|
||||
let watchdogFired = false;
|
||||
// A real deadline is required because the child may hang inside native code and never emit an event.
|
||||
const timer = setTimeout(() => {
|
||||
if (child.exitCode === null) {
|
||||
watchdogFired = true;
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
}, 25_000);
|
||||
const exited = child.exited.finally(() => clearTimeout(timer));
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let exitCode: number | null = null;
|
||||
|
||||
try {
|
||||
[stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
exited,
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
if (watchdogFired || exitCode !== 0 || stdout.trim() !== "ok") {
|
||||
throw new Error(
|
||||
`deep HTML child failed: pid=${pid}, exitCode=${exitCode}, signalCode=${child.signalCode}, watchdogFired=${watchdogFired}, stderr=${stderr}`,
|
||||
);
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
describe("MacOSPowerAssertion", () => {
|
||||
|
||||
Reference in New Issue
Block a user