aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ciro Spaciari <ciro.spaciari@gmail.com> 2023-09-15 21:18:57 -0700
committerGravatar GitHub <noreply@github.com> 2023-09-15 21:18:57 -0700
commitb54e3f3c042bc37418a646728428910cafc502ef (patch)
treef36f52c715300c6dd37add073fe76931cc98ab2d
parent7f2e40af46bdd21984b9df1695895c7f43aba482 (diff)
downloadbun-b54e3f3c042bc37418a646728428910cafc502ef.tar.gz
bun-b54e3f3c042bc37418a646728428910cafc502ef.tar.zst
bun-b54e3f3c042bc37418a646728428910cafc502ef.zip
fix(corking) uncork if needed (#5525)
* fix size limit * uncork if needed instead of terminating * undo unrelated changes
-rw-r--r--packages/bun-uws/src/AsyncSocket.h11
-rw-r--r--packages/bun-uws/src/Loop.h6
-rw-r--r--packages/bun-uws/src/LoopData.h1
-rw-r--r--test/js/workerd/html-rewriter.test.js856
4 files changed, 452 insertions, 422 deletions
diff --git a/packages/bun-uws/src/AsyncSocket.h b/packages/bun-uws/src/AsyncSocket.h
index 8e3301f24..1051271a2 100644
--- a/packages/bun-uws/src/AsyncSocket.h
+++ b/packages/bun-uws/src/AsyncSocket.h
@@ -121,18 +121,25 @@ public:
void corkUnchecked() {
/* What if another socket is corked? */
getLoopData()->corkedSocket = this;
+ getLoopData()->corkedSocketIsSSL = SSL;
}
/* Cork this socket. Only one socket may ever be corked per-loop at any given time */
void cork() {
/* Extra check for invalid corking of others */
if (getLoopData()->corkOffset && getLoopData()->corkedSocket != this) {
- std::cerr << "Error: Cork buffer must not be acquired without checking canCork!" << std::endl;
- std::terminate();
+ // We uncork the other socket early instead of terminating the program
+ // is unlikely to be cause any issues and is better than crashing
+ if(getLoopData()->corkedSocketIsSSL) {
+ ((AsyncSocket<true> *) getLoopData()->corkedSocket)->uncork();
+ } else {
+ ((AsyncSocket<false> *) getLoopData()->corkedSocket)->uncork();
+ }
}
/* What if another socket is corked? */
getLoopData()->corkedSocket = this;
+ getLoopData()->corkedSocketIsSSL = SSL;
}
/* Returns the corked socket or nullptr */
diff --git a/packages/bun-uws/src/Loop.h b/packages/bun-uws/src/Loop.h
index 6c7bdfc9e..d3ca45b58 100644
--- a/packages/bun-uws/src/Loop.h
+++ b/packages/bun-uws/src/Loop.h
@@ -58,12 +58,6 @@ private:
for (auto &p : loopData->postHandlers) {
p.second((Loop *) loop);
}
-
- /* After every event loop iteration, we must not hold the cork buffer */
- if (loopData->corkedSocket) {
- std::cerr << "Error: Cork buffer must not be held across event loop iterations!" << std::endl;
- std::terminate();
- }
}
Loop() = delete;
diff --git a/packages/bun-uws/src/LoopData.h b/packages/bun-uws/src/LoopData.h
index 986bf0cbd..92bd9ffff 100644
--- a/packages/bun-uws/src/LoopData.h
+++ b/packages/bun-uws/src/LoopData.h
@@ -98,6 +98,7 @@ public:
char *corkBuffer = new char[CORK_BUFFER_SIZE];
unsigned int corkOffset = 0;
void *corkedSocket = nullptr;
+ bool corkedSocketIsSSL = false;
/* Per message deflate data */
ZlibContext *zlibContext = nullptr;
diff --git a/test/js/workerd/html-rewriter.test.js b/test/js/workerd/html-rewriter.test.js
index b0c951197..3f96c22c1 100644
--- a/test/js/workerd/html-rewriter.test.js
+++ b/test/js/workerd/html-rewriter.test.js
@@ -14,420 +14,448 @@ var setTimeoutAsync = (fn, delay) => {
});
};
-// describe("HTMLRewriter", () => {
-// it("HTMLRewriter: async replacement", async () => {
-// await gcTick();
-// const res = new HTMLRewriter()
-// .on("div", {
-// async element(element) {
-// await setTimeoutAsync(() => {
-// element.setInnerContent("<span>replace</span>", { html: true });
-// }, 5);
-// },
-// })
-// .transform(new Response("<div>example.com</div>"));
-// await gcTick();
-// expect(await res.text()).toBe("<div><span>replace</span></div>");
-// await gcTick();
-// });
-
-// it("supports element handlers", async () => {
-// var rewriter = new HTMLRewriter();
-// rewriter.on("div", {
-// element(element) {
-// element.setInnerContent("<blink>it worked!</blink>", { html: true });
-// },
-// });
-// var input = new Response("<div>hello</div>");
-// var output = rewriter.transform(input);
-// expect(await output.text()).toBe("<div><blink>it worked!</blink></div>");
-// });
-
-// it("(from file) supports element handlers", async () => {
-// var rewriter = new HTMLRewriter();
-// rewriter.on("div", {
-// element(element) {
-// element.setInnerContent("<blink>it worked!</blink>", { html: true });
-// },
-// });
-// await Bun.write("/tmp/html-rewriter.txt.js", "<div>hello</div>");
-// var input = new Response(Bun.file("/tmp/html-rewriter.txt.js"));
-// var output = rewriter.transform(input);
-// expect(await output.text()).toBe("<div><blink>it worked!</blink></div>");
-// });
-
-// it("supports attribute iterator", async () => {
-// var rewriter = new HTMLRewriter();
-// var expected = [
-// ["first", ""],
-// ["second", "alrihgt"],
-// ["third", "123"],
-// ["fourth", "5"],
-// ["fifth", "helloooo"],
-// ];
-// rewriter.on("div", {
-// element(element2) {
-// for (let attr of element2.attributes) {
-// const stack = expected.shift();
-// expect(stack[0]).toBe(attr[0]);
-// expect(stack[1]).toBe(attr[1]);
-// }
-// },
-// });
-// var input = new Response('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>');
-// var output = rewriter.transform(input);
-// expect(await output.text()).toBe('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>');
-// expect(expected.length).toBe(0);
-// });
-
-// it("handles element specific mutations", async () => {
-// // prepend/append
-// let res = new HTMLRewriter()
-// .on("p", {
-// element(element) {
-// element.prepend("<span>prepend</span>");
-// element.prepend("<span>prepend html</span>", { html: true });
-// element.append("<span>append</span>");
-// element.append("<span>append html</span>", { html: true });
-// },
-// })
-// .transform(new Response("<p>test</p>"));
-// expect(await res.text()).toBe(
-// [
-// "<p>",
-// "<span>prepend html</span>",
-// "&lt;span&gt;prepend&lt;/span&gt;",
-// "test",
-// "&lt;span&gt;append&lt;/span&gt;",
-// "<span>append html</span>",
-// "</p>",
-// ].join(""),
-// );
-
-// // setInnerContent
-// res = new HTMLRewriter()
-// .on("p", {
-// element(element) {
-// element.setInnerContent("<span>replace</span>");
-// },
-// })
-// .transform(new Response("<p>test</p>"));
-// expect(await res.text()).toBe("<p>&lt;span&gt;replace&lt;/span&gt;</p>");
-// res = new HTMLRewriter()
-// .on("p", {
-// element(element) {
-// element.setInnerContent("<span>replace</span>", { html: true });
-// },
-// })
-// .transform(new Response("<p>test</p>"));
-// expect(await res.text()).toBe("<p><span>replace</span></p>");
-
-// // removeAndKeepContent
-// res = new HTMLRewriter()
-// .on("p", {
-// element(element) {
-// element.removeAndKeepContent();
-// },
-// })
-// .transform(new Response("<p>test</p>"));
-// expect(await res.text()).toBe("test");
-// });
-
-// it("handles element class properties", async () => {
-// class Handler {
-// constructor(content) {
-// this.content = content;
-// }
-
-// // noinspection JSUnusedGlobalSymbols
-// element(element) {
-// element.setInnerContent(this.content);
-// }
-// }
-// const res = new HTMLRewriter().on("p", new Handler("new")).transform(new Response("<p>test</p>"));
-// expect(await res.text()).toBe("<p>new</p>");
-// });
-
-// const commentsMutationsInput = "<p><!--test--></p>";
-// const commentsMutationsExpected = {
-// beforeAfter: [
-// "<p>",
-// "&lt;span&gt;before&lt;/span&gt;",
-// "<span>before html</span>",
-// "<!--test-->",
-// "<span>after html</span>",
-// "&lt;span&gt;after&lt;/span&gt;",
-// "</p>",
-// ].join(""),
-// replace: "<p>&lt;span&gt;replace&lt;/span&gt;</p>",
-// replaceHtml: "<p><span>replace</span></p>",
-// remove: "<p></p>",
-// };
-
-// const commentPropertiesMacro = async func => {
-// const res = func(new HTMLRewriter(), comment => {
-// expect(comment.removed).toBe(false);
-// expect(comment.text).toBe("test");
-// comment.text = "new";
-// expect(comment.text).toBe("new");
-// }).transform(new Response("<p><!--test--></p>"));
-// expect(await res.text()).toBe("<p><!--new--></p>");
-// };
-
-// it("HTMLRewriter: handles comment properties", () =>
-// commentPropertiesMacro((rw, comments) => {
-// rw.on("p", { comments });
-// return rw;
-// }));
-
-// it("selector tests", async () => {
-// const checkSelector = async (selector, input, expected) => {
-// const res = new HTMLRewriter()
-// .on(selector, {
-// element(element) {
-// element.setInnerContent("new");
-// },
-// })
-// .transform(new Response(input));
-// expect(await res.text()).toBe(expected);
-// };
-
-// await checkSelector("*", "<h1>1</h1><p>2</p>", "<h1>new</h1><p>new</p>");
-// await checkSelector("p", "<h1>1</h1><p>2</p>", "<h1>1</h1><p>new</p>");
-// await checkSelector(
-// "p:nth-child(2)",
-// "<div><p>1</p><p>2</p><p>3</p></div>",
-// "<div><p>1</p><p>new</p><p>3</p></div>",
-// );
-// await checkSelector(
-// "p:first-child",
-// "<div><p>1</p><p>2</p><p>3</p></div>",
-// "<div><p>new</p><p>2</p><p>3</p></div>",
-// );
-// await checkSelector(
-// "p:nth-of-type(2)",
-// "<div><p>1</p><h1>2</h1><p>3</p><h1>4</h1><p>5</p></div>",
-// "<div><p>1</p><h1>2</h1><p>new</p><h1>4</h1><p>5</p></div>",
-// );
-// await checkSelector(
-// "p:first-of-type",
-// "<div><h1>1</h1><p>2</p><p>3</p></div>",
-// "<div><h1>1</h1><p>new</p><p>3</p></div>",
-// );
-// await checkSelector(
-// "p:not(:first-child)",
-// "<div><p>1</p><p>2</p><p>3</p></div>",
-// "<div><p>1</p><p>new</p><p>new</p></div>",
-// );
-// await checkSelector("p.red", '<p class="red">1</p><p>2</p>', '<p class="red">new</p><p>2</p>');
-// await checkSelector("h1#header", '<h1 id="header">1</h1><h1>2</h1>', '<h1 id="header">new</h1><h1>2</h1>');
-// await checkSelector("p[data-test]", "<p data-test>1</p><p>2</p>", "<p data-test>new</p><p>2</p>");
-// await checkSelector(
-// 'p[data-test="one"]',
-// '<p data-test="one">1</p><p data-test="two">2</p>',
-// '<p data-test="one">new</p><p data-test="two">2</p>',
-// );
-// await checkSelector(
-// 'p[data-test="one" i]',
-// '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>',
-// '<p data-test="one">new</p><p data-test="OnE">new</p><p data-test="two">3</p>',
-// );
-// await checkSelector(
-// 'p[data-test="one" s]',
-// '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>',
-// '<p data-test="one">new</p><p data-test="OnE">2</p><p data-test="two">3</p>',
-// );
-// await checkSelector(
-// 'p[data-test~="two"]',
-// '<p data-test="one two three">1</p><p data-test="one two">2</p><p data-test="one">3</p>',
-// '<p data-test="one two three">new</p><p data-test="one two">new</p><p data-test="one">3</p>',
-// );
-// await checkSelector(
-// 'p[data-test^="a"]',
-// '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>',
-// '<p data-test="a1">new</p><p data-test="a2">new</p><p data-test="b1">3</p>',
-// );
-// await checkSelector(
-// 'p[data-test$="1"]',
-// '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>',
-// '<p data-test="a1">new</p><p data-test="a2">2</p><p data-test="b1">new</p>',
-// );
-// await checkSelector(
-// 'p[data-test*="b"]',
-// '<p data-test="abc">1</p><p data-test="ab">2</p><p data-test="a">3</p>',
-// '<p data-test="abc">new</p><p data-test="ab">new</p><p data-test="a">3</p>',
-// );
-// await checkSelector(
-// 'p[data-test|="a"]',
-// '<p data-test="a">1</p><p data-test="a-1">2</p><p data-test="a2">3</p>',
-// '<p data-test="a">new</p><p data-test="a-1">new</p><p data-test="a2">3</p>',
-// );
-// await checkSelector(
-// "div span",
-// "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>",
-// "<div><h1><span>new</span></h1><span>new</span><b>3</b></div>",
-// );
-// await checkSelector(
-// "div > span",
-// "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>",
-// "<div><h1><span>1</span></h1><span>new</span><b>3</b></div>",
-// );
-// });
-
-// it("supports deleting innerContent", async () => {
-// expect(
-// await new HTMLRewriter()
-// .on("div", {
-// element(elem) {
-// // https://github.com/oven-sh/bun/issues/2323
-// elem.setInnerContent("");
-// },
-// })
-// .transform(new Response("<div>content</div>"))
-// .text(),
-// ).toEqual("<div></div>");
-// });
-
-// it("supports deleting innerHTML", async () => {
-// expect(
-// await new HTMLRewriter()
-// .on("div", {
-// element(elem) {
-// // https://github.com/oven-sh/bun/issues/2323
-// elem.setInnerContent("", { html: true });
-// },
-// })
-// .transform(new Response("<div><span>content</span></div>"))
-// .text(),
-// ).toEqual("<div></div>");
-// });
-
-// it("it supports lastInTextNode", async () => {
-// let lastInTextNode;
-
-// await new HTMLRewriter()
-// .on("p", {
-// text(text) {
-// lastInTextNode ??= text.lastInTextNode;
-// },
-// })
-// .transform(new Response("<p>Lorem ipsum!</p>"))
-// .text();
-
-// expect(lastInTextNode).toBeBoolean();
-// });
-
-// it("it supports selfClosing", async () => {
-// const selfClosing = {};
-// await new HTMLRewriter()
-// .on("*", {
-// element(el) {
-// selfClosing[el.tagName] = el.selfClosing;
-// },
-// })
-
-// .transform(new Response("<p>Lorem ipsum!<br></p><div />"))
-// .text();
-
-// expect(selfClosing).toEqual({
-// p: false,
-// br: false,
-// div: true,
-// });
-// });
-
-// it("it supports canHaveContent", async () => {
-// const canHaveContent = {};
-// await new HTMLRewriter()
-// .on("*", {
-// element(el) {
-// canHaveContent[el.tagName] = el.canHaveContent;
-// },
-// })
-// .transform(new Response("<p>Lorem ipsum!<br></p><div /><svg><circle /></svg>"))
-// .text();
-
-// expect(canHaveContent).toEqual({
-// p: true,
-// br: false,
-// div: true,
-// svg: true,
-// circle: false,
-// });
-// });
-// });
-
-// // By not segfaulting, this test passes
-// it("#3334 regression", async () => {
-// for (let i = 0; i < 10; i++) {
-// const headers = new Headers({
-// "content-type": "text/html",
-// });
-// const response = new Response("<div>content</div>", { headers });
-
-// const result = await new HTMLRewriter()
-// .on("div", {
-// element(elem) {
-// elem.setInnerContent("new");
-// },
-// })
-// .transform(response)
-// .text();
-// expect(result).toEqual("<div>new</div>");
-// }
-// Bun.gc(true);
-// });
-
-// it("#3489", async () => {
-// var el;
-// await new HTMLRewriter()
-// .on("p", {
-// element(element) {
-// el = element.getAttribute("id");
-// },
-// })
-// .transform(new Response('<p id="Šžõäöü"></p>'))
-// .text();
-// expect(el).toEqual("Šžõäöü");
-// });
-
-// it("get attribute - ascii", async () => {
-// for (let i = 0; i < 10; i++) {
-// var el;
-// await new HTMLRewriter()
-// .on("p", {
-// element(element) {
-// el = element.getAttribute("id");
-// },
-// })
-// .transform(new Response(`<p id="asciii"></p>`))
-// .text();
-// expect(el).toEqual("asciii");
-// }
-// });
-
-// it("#3520", async () => {
-// const pairs = [];
-
-// await new HTMLRewriter()
-// .on("p", {
-// element(element) {
-// for (const pair of element.attributes) {
-// pairs.push(pair);
-// }
-// },
-// })
-// .transform(new Response('<p šž="Õäöü" ab="Õäöü" šž="Õäöü" šž="dc" šž="🕵🏻"></p>'))
-// .text();
-
-// expect(pairs).toEqual([
-// ["šž", "Õäöü"],
-// ["ab", "Õäöü"],
-// ["šž", "Õäöü"],
-// ["šž", "dc"],
-// ["šž", "🕵🏻"],
-// ]);
-// });
+describe("HTMLRewriter", () => {
+ it("HTMLRewriter: async replacement", async () => {
+ await gcTick();
+ const res = new HTMLRewriter()
+ .on("div", {
+ async element(element) {
+ await setTimeoutAsync(() => {
+ element.setInnerContent("<span>replace</span>", { html: true });
+ }, 5);
+ },
+ })
+ .transform(new Response("<div>example.com</div>"));
+ await gcTick();
+ expect(await res.text()).toBe("<div><span>replace</span></div>");
+ await gcTick();
+ });
+
+ it("HTMLRewriter: async replacement using fetch + Bun.serve", async () => {
+ await gcTick();
+ let content;
+ let server;
+ try {
+ server = Bun.serve({
+ port: 0,
+ fetch(req) {
+ return new HTMLRewriter()
+ .on("div", {
+ async element(element) {
+ content = await fetch("https://www.example.com/").then(res => res.text());
+ element.setInnerContent(content, { html: true });
+ },
+ })
+ .transform(new Response("<div>example.com</div>"));
+ },
+ });
+
+ await gcTick();
+ const url = `http://localhost:${server.port}`;
+ expect(await fetch(url).then(res => res.text())).toBe(`<div>${content}</div>`);
+ await gcTick();
+ } finally {
+ server.stop();
+ }
+ });
+
+ it("supports element handlers", async () => {
+ var rewriter = new HTMLRewriter();
+ rewriter.on("div", {
+ element(element) {
+ element.setInnerContent("<blink>it worked!</blink>", { html: true });
+ },
+ });
+ var input = new Response("<div>hello</div>");
+ var output = rewriter.transform(input);
+ expect(await output.text()).toBe("<div><blink>it worked!</blink></div>");
+ });
+
+ it("(from file) supports element handlers", async () => {
+ var rewriter = new HTMLRewriter();
+ rewriter.on("div", {
+ element(element) {
+ element.setInnerContent("<blink>it worked!</blink>", { html: true });
+ },
+ });
+ await Bun.write("/tmp/html-rewriter.txt.js", "<div>hello</div>");
+ var input = new Response(Bun.file("/tmp/html-rewriter.txt.js"));
+ var output = rewriter.transform(input);
+ expect(await output.text()).toBe("<div><blink>it worked!</blink></div>");
+ });
+
+ it("supports attribute iterator", async () => {
+ var rewriter = new HTMLRewriter();
+ var expected = [
+ ["first", ""],
+ ["second", "alrihgt"],
+ ["third", "123"],
+ ["fourth", "5"],
+ ["fifth", "helloooo"],
+ ];
+ rewriter.on("div", {
+ element(element2) {
+ for (let attr of element2.attributes) {
+ const stack = expected.shift();
+ expect(stack[0]).toBe(attr[0]);
+ expect(stack[1]).toBe(attr[1]);
+ }
+ },
+ });
+ var input = new Response('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>');
+ var output = rewriter.transform(input);
+ expect(await output.text()).toBe('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>');
+ expect(expected.length).toBe(0);
+ });
+
+ it("handles element specific mutations", async () => {
+ // prepend/append
+ let res = new HTMLRewriter()
+ .on("p", {
+ element(element) {
+ element.prepend("<span>prepend</span>");
+ element.prepend("<span>prepend html</span>", { html: true });
+ element.append("<span>append</span>");
+ element.append("<span>append html</span>", { html: true });
+ },
+ })
+ .transform(new Response("<p>test</p>"));
+ expect(await res.text()).toBe(
+ [
+ "<p>",
+ "<span>prepend html</span>",
+ "&lt;span&gt;prepend&lt;/span&gt;",
+ "test",
+ "&lt;span&gt;append&lt;/span&gt;",
+ "<span>append html</span>",
+ "</p>",
+ ].join(""),
+ );
+
+ // setInnerContent
+ res = new HTMLRewriter()
+ .on("p", {
+ element(element) {
+ element.setInnerContent("<span>replace</span>");
+ },
+ })
+ .transform(new Response("<p>test</p>"));
+ expect(await res.text()).toBe("<p>&lt;span&gt;replace&lt;/span&gt;</p>");
+ res = new HTMLRewriter()
+ .on("p", {
+ element(element) {
+ element.setInnerContent("<span>replace</span>", { html: true });
+ },
+ })
+ .transform(new Response("<p>test</p>"));
+ expect(await res.text()).toBe("<p><span>replace</span></p>");
+
+ // removeAndKeepContent
+ res = new HTMLRewriter()
+ .on("p", {
+ element(element) {
+ element.removeAndKeepContent();
+ },
+ })
+ .transform(new Response("<p>test</p>"));
+ expect(await res.text()).toBe("test");
+ });
+
+ it("handles element class properties", async () => {
+ class Handler {
+ constructor(content) {
+ this.content = content;
+ }
+
+ // noinspection JSUnusedGlobalSymbols
+ element(element) {
+ element.setInnerContent(this.content);
+ }
+ }
+ const res = new HTMLRewriter().on("p", new Handler("new")).transform(new Response("<p>test</p>"));
+ expect(await res.text()).toBe("<p>new</p>");
+ });
+
+ const commentsMutationsInput = "<p><!--test--></p>";
+ const commentsMutationsExpected = {
+ beforeAfter: [
+ "<p>",
+ "&lt;span&gt;before&lt;/span&gt;",
+ "<span>before html</span>",
+ "<!--test-->",
+ "<span>after html</span>",
+ "&lt;span&gt;after&lt;/span&gt;",
+ "</p>",
+ ].join(""),
+ replace: "<p>&lt;span&gt;replace&lt;/span&gt;</p>",
+ replaceHtml: "<p><span>replace</span></p>",
+ remove: "<p></p>",
+ };
+
+ const commentPropertiesMacro = async func => {
+ const res = func(new HTMLRewriter(), comment => {
+ expect(comment.removed).toBe(false);
+ expect(comment.text).toBe("test");
+ comment.text = "new";
+ expect(comment.text).toBe("new");
+ }).transform(new Response("<p><!--test--></p>"));
+ expect(await res.text()).toBe("<p><!--new--></p>");
+ };
+
+ it("HTMLRewriter: handles comment properties", () =>
+ commentPropertiesMacro((rw, comments) => {
+ rw.on("p", { comments });
+ return rw;
+ }));
+
+ it("selector tests", async () => {
+ const checkSelector = async (selector, input, expected) => {
+ const res = new HTMLRewriter()
+ .on(selector, {
+ element(element) {
+ element.setInnerContent("new");
+ },
+ })
+ .transform(new Response(input));
+ expect(await res.text()).toBe(expected);
+ };
+
+ await checkSelector("*", "<h1>1</h1><p>2</p>", "<h1>new</h1><p>new</p>");
+ await checkSelector("p", "<h1>1</h1><p>2</p>", "<h1>1</h1><p>new</p>");
+ await checkSelector(
+ "p:nth-child(2)",
+ "<div><p>1</p><p>2</p><p>3</p></div>",
+ "<div><p>1</p><p>new</p><p>3</p></div>",
+ );
+ await checkSelector(
+ "p:first-child",
+ "<div><p>1</p><p>2</p><p>3</p></div>",
+ "<div><p>new</p><p>2</p><p>3</p></div>",
+ );
+ await checkSelector(
+ "p:nth-of-type(2)",
+ "<div><p>1</p><h1>2</h1><p>3</p><h1>4</h1><p>5</p></div>",
+ "<div><p>1</p><h1>2</h1><p>new</p><h1>4</h1><p>5</p></div>",
+ );
+ await checkSelector(
+ "p:first-of-type",
+ "<div><h1>1</h1><p>2</p><p>3</p></div>",
+ "<div><h1>1</h1><p>new</p><p>3</p></div>",
+ );
+ await checkSelector(
+ "p:not(:first-child)",
+ "<div><p>1</p><p>2</p><p>3</p></div>",
+ "<div><p>1</p><p>new</p><p>new</p></div>",
+ );
+ await checkSelector("p.red", '<p class="red">1</p><p>2</p>', '<p class="red">new</p><p>2</p>');
+ await checkSelector("h1#header", '<h1 id="header">1</h1><h1>2</h1>', '<h1 id="header">new</h1><h1>2</h1>');
+ await checkSelector("p[data-test]", "<p data-test>1</p><p>2</p>", "<p data-test>new</p><p>2</p>");
+ await checkSelector(
+ 'p[data-test="one"]',
+ '<p data-test="one">1</p><p data-test="two">2</p>',
+ '<p data-test="one">new</p><p data-test="two">2</p>',
+ );
+ await checkSelector(
+ 'p[data-test="one" i]',
+ '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>',
+ '<p data-test="one">new</p><p data-test="OnE">new</p><p data-test="two">3</p>',
+ );
+ await checkSelector(
+ 'p[data-test="one" s]',
+ '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>',
+ '<p data-test="one">new</p><p data-test="OnE">2</p><p data-test="two">3</p>',
+ );
+ await checkSelector(
+ 'p[data-test~="two"]',
+ '<p data-test="one two three">1</p><p data-test="one two">2</p><p data-test="one">3</p>',
+ '<p data-test="one two three">new</p><p data-test="one two">new</p><p data-test="one">3</p>',
+ );
+ await checkSelector(
+ 'p[data-test^="a"]',
+ '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>',
+ '<p data-test="a1">new</p><p data-test="a2">new</p><p data-test="b1">3</p>',
+ );
+ await checkSelector(
+ 'p[data-test$="1"]',
+ '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>',
+ '<p data-test="a1">new</p><p data-test="a2">2</p><p data-test="b1">new</p>',
+ );
+ await checkSelector(
+ 'p[data-test*="b"]',
+ '<p data-test="abc">1</p><p data-test="ab">2</p><p data-test="a">3</p>',
+ '<p data-test="abc">new</p><p data-test="ab">new</p><p data-test="a">3</p>',
+ );
+ await checkSelector(
+ 'p[data-test|="a"]',
+ '<p data-test="a">1</p><p data-test="a-1">2</p><p data-test="a2">3</p>',
+ '<p data-test="a">new</p><p data-test="a-1">new</p><p data-test="a2">3</p>',
+ );
+ await checkSelector(
+ "div span",
+ "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>",
+ "<div><h1><span>new</span></h1><span>new</span><b>3</b></div>",
+ );
+ await checkSelector(
+ "div > span",
+ "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>",
+ "<div><h1><span>1</span></h1><span>new</span><b>3</b></div>",
+ );
+ });
+
+ it("supports deleting innerContent", async () => {
+ expect(
+ await new HTMLRewriter()
+ .on("div", {
+ element(elem) {
+ // https://github.com/oven-sh/bun/issues/2323
+ elem.setInnerContent("");
+ },
+ })
+ .transform(new Response("<div>content</div>"))
+ .text(),
+ ).toEqual("<div></div>");
+ });
+
+ it("supports deleting innerHTML", async () => {
+ expect(
+ await new HTMLRewriter()
+ .on("div", {
+ element(elem) {
+ // https://github.com/oven-sh/bun/issues/2323
+ elem.setInnerContent("", { html: true });
+ },
+ })
+ .transform(new Response("<div><span>content</span></div>"))
+ .text(),
+ ).toEqual("<div></div>");
+ });
+
+ it("it supports lastInTextNode", async () => {
+ let lastInTextNode;
+
+ await new HTMLRewriter()
+ .on("p", {
+ text(text) {
+ lastInTextNode ??= text.lastInTextNode;
+ },
+ })
+ .transform(new Response("<p>Lorem ipsum!</p>"))
+ .text();
+
+ expect(lastInTextNode).toBeBoolean();
+ });
+
+ it("it supports selfClosing", async () => {
+ const selfClosing = {};
+ await new HTMLRewriter()
+ .on("*", {
+ element(el) {
+ selfClosing[el.tagName] = el.selfClosing;
+ },
+ })
+
+ .transform(new Response("<p>Lorem ipsum!<br></p><div />"))
+ .text();
+
+ expect(selfClosing).toEqual({
+ p: false,
+ br: false,
+ div: true,
+ });
+ });
+
+ it("it supports canHaveContent", async () => {
+ const canHaveContent = {};
+ await new HTMLRewriter()
+ .on("*", {
+ element(el) {
+ canHaveContent[el.tagName] = el.canHaveContent;
+ },
+ })
+ .transform(new Response("<p>Lorem ipsum!<br></p><div /><svg><circle /></svg>"))
+ .text();
+
+ expect(canHaveContent).toEqual({
+ p: true,
+ br: false,
+ div: true,
+ svg: true,
+ circle: false,
+ });
+ });
+});
+
+// By not segfaulting, this test passes
+it("#3334 regression", async () => {
+ for (let i = 0; i < 10; i++) {
+ const headers = new Headers({
+ "content-type": "text/html",
+ });
+ const response = new Response("<div>content</div>", { headers });
+
+ const result = await new HTMLRewriter()
+ .on("div", {
+ element(elem) {
+ elem.setInnerContent("new");
+ },
+ })
+ .transform(response)
+ .text();
+ expect(result).toEqual("<div>new</div>");
+ }
+ Bun.gc(true);
+});
+
+it("#3489", async () => {
+ var el;
+ await new HTMLRewriter()
+ .on("p", {
+ element(element) {
+ el = element.getAttribute("id");
+ },
+ })
+ .transform(new Response('<p id="Šžõäöü"></p>'))
+ .text();
+ expect(el).toEqual("Šžõäöü");
+});
+
+it("get attribute - ascii", async () => {
+ for (let i = 0; i < 10; i++) {
+ var el;
+ await new HTMLRewriter()
+ .on("p", {
+ element(element) {
+ el = element.getAttribute("id");
+ },
+ })
+ .transform(new Response(`<p id="asciii"></p>`))
+ .text();
+ expect(el).toEqual("asciii");
+ }
+});
+
+it("#3520", async () => {
+ const pairs = [];
+
+ await new HTMLRewriter()
+ .on("p", {
+ element(element) {
+ for (const pair of element.attributes) {
+ pairs.push(pair);
+ }
+ },
+ })
+ .transform(new Response('<p šž="Õäöü" ab="Õäöü" šž="Õäöü" šž="dc" šž="🕵🏻"></p>'))
+ .text();
+
+ expect(pairs).toEqual([
+ ["šž", "Õäöü"],
+ ["ab", "Õäöü"],
+ ["šž", "Õäöü"],
+ ["šž", "dc"],
+ ["šž", "🕵🏻"],
+ ]);
+});
const fixture_html = path.join(import.meta.dir, "../web/fetch/fixture.html");
const fixture_html_content = fs.readFileSync(fixture_html);