lol_async/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    missing_copy_implementations,
4    rustdoc::missing_crate_level_docs,
5    missing_debug_implementations,
6    missing_docs,
7    nonstandard_style,
8    unused_qualifications
9)]
10
11/*!
12This crate is an effort to adapt
13[`cloudflare/lol-html`](https://github.com/cloudflare/lol-html) for an
14async context. Unfortunately, due to lol-html's design, the wrapped
15api is not ideal. In particular, the [`lol_html::HtmlRewriter`] is
16`!Send`, which means that we have some contortions in order to use
17this library in an async context that expects most futures to be
18`Send`. This crate addresses this by returning two types: A
19[`LolFuture`] that must be polled on whatever thread calls [`rewrite`]
20and a [`LolReader`] that is [`AsyncRead`] and can be moved around and
21sent between threads as needed.
22
23❗ **Due to this design, it is necessary to poll the [`LolFuture`] in
24addition to reading from the [`LolReader`]** ❗
25
26## Improvements
27
28Improvements to the design of this crate are very welcome. I don't
29have a lot of experience working around `!Send` types in an async
30context, and although this crate achieved the result I needed, I hate
31it. Please open a PR or write a better crate! Alternatively, if you
32know someone at cloudflare, maybe arms can be twisted to adapt
33`lol-html` for async rust.
34
35```
36# use async_global_executor as your_async_executor;
37# use futures_lite::{io::Cursor, AsyncReadExt};
38use lol_async::html::{element, html_content::ContentType, Settings};
39
40# your_async_executor::block_on(async {
41let (fut, mut reader) = lol_async::rewrite(
42    Cursor::new(r#"<html>
43<head><title>hello lol</title></head>
44<body><h1>hey there</h1></body>
45</html>"#),
46    Settings {
47        element_content_handlers: vec![element!("h1", |el| {
48            el.append("<span>this was inserted</span>", ContentType::Html);
49            Ok(())
50        })],
51        ..Settings::default()
52    }
53);
54
55let handle = your_async_executor::spawn_local(fut);
56
57let mut buf = String::new();
58reader.read_to_string(&mut buf).await?;
59
60handle.await?;
61assert_eq!(buf, r#"<html>
62<head><title>hello lol</title></head>
63<body><h1>hey there<span>this was inserted</span></h1></body>
64</html>"#);
65# Result::<_, Box<dyn std::error::Error>>::Ok(()) }).unwrap();
66```
67
68
69*/
70use atomic_waker::AtomicWaker;
71use futures_lite::{ready, AsyncRead};
72use lockfree::queue::Queue;
73use lol_html::{HtmlRewriter, OutputSink, Settings};
74use pin_project_lite::pin_project;
75use std::{
76    fmt::{self, Debug},
77    future::Future,
78    io::Result,
79    pin::Pin,
80    sync::{
81        atomic::{AtomicBool, Ordering},
82        Arc,
83    },
84    task::{Context, Poll},
85};
86
87pub use lol_html as html;
88
89impl<R> Debug for LolFuture<'_, R> {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.debug_struct("LolFuture")
92            .field("buffer", &self.buffer)
93            .finish()
94    }
95}
96
97impl Debug for LolReader {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.debug_struct("LolReader")
100            .field("waker", &self.waker)
101            .field("done", &self.done)
102            .finish()
103    }
104}
105
106/**
107An [`AsyncRead`] type that will yield the rewritten html. LolReader is
108Send, allowing it to be used on a different thread from the paired
109[`LolFuture`]. Please note that reading from LolReader does not drive the
110LolHtml rewriter. Awaiting the [`LolFuture`] is also necessary.
111*/
112
113pub struct LolReader {
114    waker: Arc<AtomicWaker>,
115    done: Arc<AtomicBool>,
116    data: Arc<Queue<u8>>,
117}
118
119struct LolOutputter {
120    done: Arc<AtomicBool>,
121    waker: Arc<AtomicWaker>,
122    data: Arc<Queue<u8>>,
123}
124
125pin_project! {
126    /**
127    `await` this [`Future`] to drive the html rewriting process. The
128    LolFuture contains the [`HtmlRewriter`] and as a result is
129    `!Send`, so it must be spawned locally.
130    */
131    pub struct LolFuture<'h, R> {
132        #[pin] source: R,
133        rewriter: Option<HtmlRewriter<'h, LolOutputter>>,
134        buffer: Vec<u8>
135    }
136}
137
138/**
139This function is the primary entrypoint for `lol-async`. It takes a
140data `Source` that is [`AsyncRead`] and a [`Settings`] that describes
141the desired rewriting logic. It returns a !Send [`LolFuture`] future
142that drives the rewriter on the current thread and a [`LolReader`]
143that is Send and can be used anywhere an [`AsyncRead`] would be
144used. The html content yielded by the [`LolReader`] will be rewritten
145according to the rules specified in the Settings.
146*/
147pub fn rewrite<'h, Source>(
148    source: Source,
149    settings: Settings<'h, '_>,
150) -> (LolFuture<'h, Source>, LolReader)
151where
152    Source: AsyncRead,
153{
154    let waker = Arc::new(AtomicWaker::new());
155    let done = Arc::new(AtomicBool::new(false));
156    let data = Arc::new(Queue::new());
157
158    let output_sink = LolOutputter {
159        waker: waker.clone(),
160        done: done.clone(),
161        data: data.clone(),
162    };
163
164    let rewriter = HtmlRewriter::new(settings, output_sink);
165
166    let future = LolFuture {
167        source,
168        rewriter: Some(rewriter),
169        buffer: vec![0; 1024],
170    };
171
172    let reader = LolReader { waker, done, data };
173
174    (future, reader)
175}
176
177impl<Source: AsyncRead> Future for LolFuture<'_, Source> {
178    type Output = Result<()>;
179    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
180        let mut this = self.project();
181        loop {
182            match this.rewriter {
183                Some(rewriter) => match ready!(this.source.as_mut().poll_read(cx, this.buffer))? {
184                    0 => {
185                        this.rewriter.take().unwrap().end().unwrap();
186                    }
187
188                    new_bytes => {
189                        rewriter.write(&this.buffer[..new_bytes]).unwrap();
190                    }
191                },
192
193                None => return Poll::Ready(Ok(())),
194            }
195        }
196    }
197}
198
199impl OutputSink for LolOutputter {
200    fn handle_chunk(&mut self, chunk: &[u8]) {
201        if chunk.is_empty() {
202            self.done.store(true, Ordering::SeqCst);
203        } else {
204            self.data.extend(chunk.to_vec());
205        }
206
207        self.waker.wake();
208    }
209}
210
211impl AsyncRead for LolReader {
212    fn poll_read(
213        self: Pin<&mut Self>,
214        cx: &mut Context<'_>,
215        buf: &mut [u8],
216    ) -> Poll<Result<usize>> {
217        let data: Vec<u8> = self.data.pop_iter().take(buf.len()).collect();
218        let len = data.len();
219        if len > 0 {
220            buf[..len].copy_from_slice(&data);
221            Poll::Ready(Ok(len))
222        } else if self.done.load(Ordering::SeqCst) {
223            Poll::Ready(Ok(0))
224        } else {
225            self.waker.register(cx.waker());
226            Poll::Pending
227        }
228    }
229}