turbopack_core/resolve/
parse.rs

1use anyhow::{Ok, Result};
2use lazy_static::lazy_static;
3use regex::Regex;
4use turbo_rcstr::RcStr;
5use turbo_tasks::{ResolvedVc, TryJoinIterExt, Value, ValueToString, Vc};
6
7use super::pattern::Pattern;
8
9#[turbo_tasks::value(shared)]
10#[derive(Hash, Clone, Debug)]
11pub enum Request {
12    Raw {
13        path: Pattern,
14        query: ResolvedVc<RcStr>,
15        force_in_lookup_dir: bool,
16        fragment: ResolvedVc<RcStr>,
17    },
18    Relative {
19        path: Pattern,
20        query: ResolvedVc<RcStr>,
21        force_in_lookup_dir: bool,
22        fragment: ResolvedVc<RcStr>,
23    },
24    Module {
25        module: RcStr,
26        path: Pattern,
27        query: ResolvedVc<RcStr>,
28        fragment: ResolvedVc<RcStr>,
29    },
30    ServerRelative {
31        path: Pattern,
32        query: ResolvedVc<RcStr>,
33        fragment: ResolvedVc<RcStr>,
34    },
35    Windows {
36        path: Pattern,
37        query: ResolvedVc<RcStr>,
38        fragment: ResolvedVc<RcStr>,
39    },
40    Empty,
41    PackageInternal {
42        path: Pattern,
43    },
44    Uri {
45        protocol: RcStr,
46        remainder: RcStr,
47        query: ResolvedVc<RcStr>,
48        fragment: ResolvedVc<RcStr>,
49    },
50    DataUri {
51        media_type: RcStr,
52        encoding: RcStr,
53        data: ResolvedVc<RcStr>,
54    },
55    Unknown {
56        path: Pattern,
57    },
58    Dynamic,
59    Alternatives {
60        requests: Vec<ResolvedVc<Request>>,
61    },
62}
63
64fn split_off_query_fragment(raw: RcStr) -> (Pattern, Vc<RcStr>, Vc<RcStr>) {
65    let Some((raw, query)) = raw.split_once('?') else {
66        if let Some((raw, fragment)) = raw.split_once('#') {
67            return (
68                Pattern::Constant(raw.into()),
69                Vc::<RcStr>::default(),
70                Vc::cell(fragment.into()),
71            );
72        }
73
74        return (
75            Pattern::Constant(raw),
76            Vc::<RcStr>::default(),
77            Vc::<RcStr>::default(),
78        );
79    };
80
81    let (query, fragment) = query.split_once('#').unwrap_or((query, ""));
82
83    (
84        Pattern::Constant(raw.into()),
85        Vc::cell(format!("?{query}").into()),
86        Vc::cell(format!("#{fragment}").into()),
87    )
88}
89
90lazy_static! {
91    static ref WINDOWS_PATH: Regex = Regex::new(r"^[A-Za-z]:\\|\\\\").unwrap();
92    static ref URI_PATH: Regex = Regex::new(r"^([^/\\:]+:)(.+)$").unwrap();
93    static ref DATA_URI_REMAINDER: Regex = Regex::new(r"^([^;,]*)(?:;([^,]+))?,(.*)$").unwrap();
94    static ref MODULE_PATH: Regex = Regex::new(r"^((?:@[^/]+/)?[^/]+)(.*)$").unwrap();
95}
96
97impl Request {
98    /// Turns the request into a string.
99    ///
100    /// Note that this is only returns something for the most basic and
101    /// fully constant patterns.
102    pub fn request(&self) -> Option<RcStr> {
103        Some(match self {
104            Request::Raw {
105                path: Pattern::Constant(path),
106                ..
107            } => path.clone(),
108            Request::Relative {
109                path: Pattern::Constant(path),
110                ..
111            } => path.clone(),
112            Request::Module {
113                module,
114                path: Pattern::Constant(path),
115                ..
116            } => format!("{module}{path}").into(),
117            Request::ServerRelative {
118                path: Pattern::Constant(path),
119                ..
120            } => path.clone(),
121            Request::Windows {
122                path: Pattern::Constant(path),
123                ..
124            } => path.clone(),
125            Request::Empty => "".into(),
126            Request::PackageInternal {
127                path: Pattern::Constant(path),
128                ..
129            } => path.clone(),
130            Request::Uri {
131                protocol,
132                remainder,
133                ..
134            } => format!("{protocol}{remainder}").into(),
135            Request::Unknown {
136                path: Pattern::Constant(path),
137            } => path.clone(),
138            _ => return None,
139        })
140    }
141
142    pub async fn parse_ref(mut request: Pattern) -> Result<Self> {
143        request.normalize();
144        Ok(match request {
145            Pattern::Dynamic => Request::Dynamic,
146            Pattern::Constant(r) => Request::parse_constant_pattern(r).await?,
147            Pattern::Concatenation(list) => Request::parse_concatenation_pattern(list).await?,
148            Pattern::Alternatives(list) => Request::parse_alternatives_pattern(list).await?,
149        })
150    }
151
152    async fn parse_constant_pattern(r: RcStr) -> Result<Self> {
153        if r.is_empty() {
154            return Ok(Request::Empty);
155        }
156
157        if let Some(remainder) = r.strip_prefix("//") {
158            return Ok(Request::Uri {
159                protocol: "//".into(),
160                remainder: remainder.into(),
161                query: ResolvedVc::cell(RcStr::default()),
162                fragment: ResolvedVc::cell(RcStr::default()),
163            });
164        }
165
166        if r.starts_with('/') {
167            let (path, query, fragment) = split_off_query_fragment(r);
168
169            return Ok(Request::ServerRelative {
170                path,
171                query: query.to_resolved().await?,
172                fragment: fragment.to_resolved().await?,
173            });
174        }
175
176        if r.starts_with('#') {
177            return Ok(Request::PackageInternal {
178                path: Pattern::Constant(r),
179            });
180        }
181
182        if r.starts_with("./") || r.starts_with("../") || r == "." || r == ".." {
183            let (path, query, fragment) = split_off_query_fragment(r);
184
185            return Ok(Request::Relative {
186                path,
187                force_in_lookup_dir: false,
188                query: query.to_resolved().await?,
189                fragment: fragment.to_resolved().await?,
190            });
191        }
192
193        if WINDOWS_PATH.is_match(&r) {
194            let (path, query, fragment) = split_off_query_fragment(r);
195
196            return Ok(Request::Windows {
197                path,
198                query: query.to_resolved().await?,
199                fragment: fragment.to_resolved().await?,
200            });
201        }
202
203        if let Some(caps) = URI_PATH.captures(&r) {
204            if let (Some(protocol), Some(remainder)) = (caps.get(1), caps.get(2)) {
205                if let Some(caps) = DATA_URI_REMAINDER.captures(remainder.as_str()) {
206                    let media_type = caps.get(1).map_or("", |m| m.as_str()).into();
207                    let encoding = caps.get(2).map_or("", |e| e.as_str()).into();
208                    let data = caps.get(3).map_or("", |d| d.as_str()).into();
209
210                    return Ok(Request::DataUri {
211                        media_type,
212                        encoding,
213                        data: ResolvedVc::cell(data),
214                    });
215                }
216
217                return Ok(Request::Uri {
218                    protocol: protocol.as_str().into(),
219                    remainder: remainder.as_str().into(),
220                    query: ResolvedVc::cell(RcStr::default()),
221                    fragment: ResolvedVc::cell(RcStr::default()),
222                });
223            }
224        }
225
226        if let Some((module, path)) = MODULE_PATH
227            .captures(&r)
228            .and_then(|caps| caps.get(1).zip(caps.get(2)))
229        {
230            let (path, query, fragment) = split_off_query_fragment(path.as_str().into());
231
232            return Ok(Request::Module {
233                module: module.as_str().into(),
234                path,
235                query: query.to_resolved().await?,
236                fragment: fragment.to_resolved().await?,
237            });
238        }
239
240        Ok(Request::Unknown {
241            path: Pattern::Constant(r),
242        })
243    }
244
245    async fn parse_concatenation_pattern(list: Vec<Pattern>) -> Result<Self> {
246        if list.is_empty() {
247            return Ok(Request::Empty);
248        }
249
250        let mut result = Box::pin(Self::parse_ref(list[0].clone())).await?;
251
252        for item in list.into_iter().skip(1) {
253            match &mut result {
254                Request::Raw { path, .. } => {
255                    path.push(item);
256                }
257                Request::Relative { path, .. } => {
258                    path.push(item);
259                }
260                Request::Module { path, .. } => {
261                    path.push(item);
262                }
263                Request::ServerRelative { path, .. } => {
264                    path.push(item);
265                }
266                Request::Windows { path, .. } => {
267                    path.push(item);
268                }
269                Request::Empty => {
270                    result = Box::pin(Self::parse_ref(item)).await?;
271                }
272                Request::PackageInternal { path } => {
273                    path.push(item);
274                }
275                Request::DataUri { .. } => {
276                    result = Request::Dynamic;
277                }
278                Request::Uri { .. } => {
279                    result = Request::Dynamic;
280                }
281                Request::Unknown { path } => {
282                    path.push(item);
283                }
284                Request::Dynamic => {}
285                Request::Alternatives { .. } => unreachable!(),
286            };
287        }
288
289        Ok(result)
290    }
291
292    async fn parse_alternatives_pattern(list: Vec<Pattern>) -> Result<Self> {
293        Ok(Request::Alternatives {
294            requests: list
295                .into_iter()
296                .map(Value::new)
297                .map(Request::parse)
298                .map(|v| async move { v.to_resolved().await })
299                .try_join()
300                .await?,
301        })
302    }
303}
304
305#[turbo_tasks::value_impl]
306impl Request {
307    #[turbo_tasks::function]
308    pub async fn parse(request: Value<Pattern>) -> Result<Vc<Self>> {
309        Ok(Self::cell(Request::parse_ref(request.into_value()).await?))
310    }
311
312    #[turbo_tasks::function]
313    pub async fn parse_string(request: RcStr) -> Result<Vc<Self>> {
314        Ok(Self::cell(Request::parse_ref(request.into()).await?))
315    }
316
317    #[turbo_tasks::function]
318    pub async fn raw(
319        request: Value<Pattern>,
320        query: Vc<RcStr>,
321        fragment: Vc<RcStr>,
322        force_in_lookup_dir: bool,
323    ) -> Result<Vc<Self>> {
324        Ok(Self::cell(Request::Raw {
325            path: request.into_value(),
326            force_in_lookup_dir,
327            query: query.to_resolved().await?,
328            fragment: fragment.to_resolved().await?,
329        }))
330    }
331
332    #[turbo_tasks::function]
333    pub async fn relative(
334        request: Value<Pattern>,
335        query: Vc<RcStr>,
336        fragment: Vc<RcStr>,
337        force_in_lookup_dir: bool,
338    ) -> Result<Vc<Self>> {
339        Ok(Self::cell(Request::Relative {
340            path: request.into_value(),
341            force_in_lookup_dir,
342            query: query.to_resolved().await?,
343            fragment: fragment.to_resolved().await?,
344        }))
345    }
346
347    #[turbo_tasks::function]
348    pub async fn module(
349        module: RcStr,
350        path: Value<Pattern>,
351        query: Vc<RcStr>,
352        fragment: Vc<RcStr>,
353    ) -> Result<Vc<Self>> {
354        Ok(Self::cell(Request::Module {
355            module,
356            path: path.into_value(),
357            query: query.to_resolved().await?,
358            fragment: fragment.to_resolved().await?,
359        }))
360    }
361
362    #[turbo_tasks::function]
363    pub async fn as_relative(self: Vc<Self>) -> Result<Vc<Self>> {
364        let this = self.await?;
365        Ok(match &*this {
366            Request::Empty
367            | Request::Raw { .. }
368            | Request::ServerRelative { .. }
369            | Request::Windows { .. }
370            | Request::Relative { .. }
371            | Request::DataUri { .. }
372            | Request::Uri { .. }
373            | Request::Dynamic => self,
374            Request::Module {
375                module,
376                path,
377                query: _,
378                fragment: _,
379            } => {
380                let mut pat = Pattern::Constant(format!("./{module}").into());
381                pat.push(path.clone());
382                // TODO add query
383                Self::parse(Value::new(pat))
384            }
385            Request::PackageInternal { path } => {
386                let mut pat = Pattern::Constant("./".into());
387                pat.push(path.clone());
388                Self::parse(Value::new(pat))
389            }
390            Request::Unknown { path } => {
391                let mut pat = Pattern::Constant("./".into());
392                pat.push(path.clone());
393                Self::parse(Value::new(pat))
394            }
395            Request::Alternatives { requests } => {
396                let requests = requests
397                    .iter()
398                    .copied()
399                    .map(|v| *v)
400                    .map(Request::as_relative)
401                    .map(|v| async move { v.to_resolved().await })
402                    .try_join()
403                    .await?;
404                Request::Alternatives { requests }.cell()
405            }
406        })
407    }
408
409    #[turbo_tasks::function]
410    pub async fn with_query(self: Vc<Self>, query: Vc<RcStr>) -> Result<Vc<Self>> {
411        Ok(match &*self.await? {
412            Request::Raw {
413                path,
414                query: _,
415                force_in_lookup_dir,
416                fragment,
417            } => Request::Raw {
418                path: path.clone(),
419                query: query.to_resolved().await?,
420                force_in_lookup_dir: *force_in_lookup_dir,
421                fragment: *fragment,
422            }
423            .cell(),
424            Request::Relative {
425                path,
426                query: _,
427                force_in_lookup_dir,
428                fragment,
429            } => Request::Relative {
430                path: path.clone(),
431                query: query.to_resolved().await?,
432                force_in_lookup_dir: *force_in_lookup_dir,
433                fragment: *fragment,
434            }
435            .cell(),
436            Request::Module {
437                module,
438                path,
439                query: _,
440                fragment,
441            } => Request::Module {
442                module: module.clone(),
443                path: path.clone(),
444                query: query.to_resolved().await?,
445                fragment: *fragment,
446            }
447            .cell(),
448            Request::ServerRelative {
449                path,
450                query: _,
451                fragment,
452            } => Request::ServerRelative {
453                path: path.clone(),
454                query: query.to_resolved().await?,
455                fragment: *fragment,
456            }
457            .cell(),
458            Request::Windows {
459                path,
460                query: _,
461                fragment,
462            } => Request::Windows {
463                path: path.clone(),
464                query: query.to_resolved().await?,
465                fragment: *fragment,
466            }
467            .cell(),
468            Request::Empty => self,
469            Request::PackageInternal { .. } => self,
470            Request::DataUri { .. } => self,
471            Request::Uri { .. } => self,
472            Request::Unknown { .. } => self,
473            Request::Dynamic => self,
474            Request::Alternatives { requests } => {
475                let requests = requests
476                    .iter()
477                    .copied()
478                    .map(|req| req.with_query(query))
479                    .map(|v| async move { v.to_resolved().await })
480                    .try_join()
481                    .await?;
482                Request::Alternatives { requests }.cell()
483            }
484        })
485    }
486
487    #[turbo_tasks::function]
488    pub async fn with_fragment(self: Vc<Self>, fragment: Vc<RcStr>) -> Result<Vc<Self>> {
489        Ok(match &*self.await? {
490            Request::Raw {
491                path,
492                query,
493                force_in_lookup_dir,
494                fragment: _,
495            } => Request::Raw {
496                path: path.clone(),
497                query: *query,
498                force_in_lookup_dir: *force_in_lookup_dir,
499                fragment: fragment.to_resolved().await?,
500            }
501            .cell(),
502            Request::Relative {
503                path,
504                query,
505                force_in_lookup_dir,
506                fragment: _,
507            } => Request::Relative {
508                path: path.clone(),
509                query: *query,
510                force_in_lookup_dir: *force_in_lookup_dir,
511                fragment: fragment.to_resolved().await?,
512            }
513            .cell(),
514            Request::Module {
515                module,
516                path,
517                query,
518                fragment: _,
519            } => Request::Module {
520                module: module.clone(),
521                path: path.clone(),
522                query: *query,
523                fragment: fragment.to_resolved().await?,
524            }
525            .cell(),
526            Request::ServerRelative {
527                path,
528                query,
529                fragment: _,
530            } => Request::ServerRelative {
531                path: path.clone(),
532                query: *query,
533                fragment: fragment.to_resolved().await?,
534            }
535            .cell(),
536            Request::Windows {
537                path,
538                query,
539                fragment: _,
540            } => Request::Windows {
541                path: path.clone(),
542                query: *query,
543                fragment: fragment.to_resolved().await?,
544            }
545            .cell(),
546            Request::Empty => self,
547            Request::PackageInternal { .. } => self,
548            Request::DataUri { .. } => self,
549            Request::Uri { .. } => self,
550            Request::Unknown { .. } => self,
551            Request::Dynamic => self,
552            Request::Alternatives { requests } => {
553                let requests = requests
554                    .iter()
555                    .copied()
556                    .map(|req| req.with_fragment(fragment))
557                    .map(|v| async move { v.to_resolved().await })
558                    .try_join()
559                    .await?;
560                Request::Alternatives { requests }.cell()
561            }
562        })
563    }
564
565    #[turbo_tasks::function]
566    pub async fn append_path(self: Vc<Self>, suffix: RcStr) -> Result<Vc<Self>> {
567        Ok(match &*self.await? {
568            Request::Raw {
569                path,
570                query,
571                force_in_lookup_dir,
572                fragment,
573            } => {
574                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
575                pat.normalize();
576                Self::raw(Value::new(pat), **query, **fragment, *force_in_lookup_dir)
577            }
578            Request::Relative {
579                path,
580                query,
581                force_in_lookup_dir,
582                fragment,
583            } => {
584                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
585                pat.normalize();
586                Self::relative(Value::new(pat), **query, **fragment, *force_in_lookup_dir)
587            }
588            Request::Module {
589                module,
590                path,
591                query,
592                fragment,
593            } => {
594                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
595                pat.normalize();
596                Self::module(module.clone(), Value::new(pat), **query, **fragment)
597            }
598            Request::ServerRelative {
599                path,
600                query,
601                fragment,
602            } => {
603                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
604                pat.normalize();
605                Self::ServerRelative {
606                    path: pat,
607                    query: *query,
608                    fragment: *fragment,
609                }
610                .cell()
611            }
612            Request::Windows {
613                path,
614                query,
615                fragment,
616            } => {
617                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
618                pat.normalize();
619                Self::Windows {
620                    path: pat,
621                    query: *query,
622                    fragment: *fragment,
623                }
624                .cell()
625            }
626            Request::Empty => Self::parse(Value::new(suffix.into())),
627            Request::PackageInternal { path } => {
628                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
629                pat.normalize();
630                Self::PackageInternal { path: pat }.cell()
631            }
632            Request::DataUri {
633                media_type,
634                encoding,
635                data,
636            } => {
637                let data = ResolvedVc::cell(format!("{}{}", data.await?, suffix).into());
638                Self::DataUri {
639                    media_type: media_type.clone(),
640                    encoding: encoding.clone(),
641                    data,
642                }
643                .cell()
644            }
645            Request::Uri {
646                protocol,
647                remainder,
648                query,
649                fragment,
650            } => {
651                let remainder = format!("{remainder}{suffix}").into();
652                Self::Uri {
653                    protocol: protocol.clone(),
654                    remainder,
655                    query: *query,
656                    fragment: *fragment,
657                }
658                .cell()
659            }
660            Request::Unknown { path } => {
661                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
662                pat.normalize();
663                Self::Unknown { path: pat }.cell()
664            }
665            Request::Dynamic => self,
666            Request::Alternatives { requests } => {
667                let requests = requests
668                    .iter()
669                    .map(|req| async { req.append_path(suffix.clone()).to_resolved().await })
670                    .try_join()
671                    .await?;
672                Request::Alternatives { requests }.cell()
673            }
674        })
675    }
676
677    #[turbo_tasks::function]
678    pub fn query(&self) -> Vc<RcStr> {
679        match self {
680            Request::Raw { query, .. } => **query,
681            Request::Relative { query, .. } => **query,
682            Request::Module { query, .. } => **query,
683            Request::ServerRelative { query, .. } => **query,
684            Request::Windows { query, .. } => **query,
685            Request::Empty => Vc::<RcStr>::default(),
686            Request::PackageInternal { .. } => Vc::<RcStr>::default(),
687            Request::DataUri { .. } => Vc::<RcStr>::default(),
688            Request::Uri { .. } => Vc::<RcStr>::default(),
689            Request::Unknown { .. } => Vc::<RcStr>::default(),
690            Request::Dynamic => Vc::<RcStr>::default(),
691            // TODO: is this correct, should we return the first one instead?
692            Request::Alternatives { .. } => Vc::<RcStr>::default(),
693        }
694    }
695
696    /// Turns the request into a pattern, similar to [Request::request()] but
697    /// more complete.
698    #[turbo_tasks::function]
699    pub async fn request_pattern(self: Vc<Self>) -> Result<Vc<Pattern>> {
700        Ok(match &*self.await? {
701            Request::Raw { path, .. } => path.clone(),
702            Request::Relative { path, .. } => path.clone(),
703            Request::Module { module, path, .. } => {
704                let mut path = path.clone();
705                path.push_front(Pattern::Constant(module.clone()));
706                path.normalize();
707                path
708            }
709            Request::ServerRelative { path, .. } => path.clone(),
710            Request::Windows { path, .. } => path.clone(),
711            Request::Empty => Pattern::Constant("".into()),
712            Request::PackageInternal { path } => path.clone(),
713            Request::DataUri {
714                media_type,
715                encoding,
716                data,
717            } => Pattern::Constant(
718                stringify_data_uri(media_type, encoding, *data)
719                    .await?
720                    .into(),
721            ),
722            Request::Uri {
723                protocol,
724                remainder,
725                ..
726            } => Pattern::Constant(format!("{protocol}{remainder}").into()),
727            Request::Unknown { path } => path.clone(),
728            Request::Dynamic => Pattern::Dynamic,
729            Request::Alternatives { requests } => Pattern::Alternatives(
730                requests
731                    .iter()
732                    .map(async |r: &ResolvedVc<Request>| -> Result<Pattern> {
733                        Ok(r.request_pattern().owned().await?)
734                    })
735                    .try_join()
736                    .await?,
737            ),
738        }
739        .cell())
740    }
741}
742
743#[turbo_tasks::value_impl]
744impl ValueToString for Request {
745    #[turbo_tasks::function]
746    async fn to_string(&self) -> Result<Vc<RcStr>> {
747        Ok(Vc::cell(match self {
748            Request::Raw {
749                path,
750                force_in_lookup_dir,
751                ..
752            } => {
753                if *force_in_lookup_dir {
754                    format!("in-lookup-dir {path}").into()
755                } else {
756                    format!("{path}").into()
757                }
758            }
759            Request::Relative {
760                path,
761                force_in_lookup_dir,
762                ..
763            } => {
764                if *force_in_lookup_dir {
765                    format!("relative-in-lookup-dir {path}").into()
766                } else {
767                    format!("relative {path}").into()
768                }
769            }
770            Request::Module { module, path, .. } => {
771                if path.could_match_others("") {
772                    format!("module \"{module}\" with subpath {path}").into()
773                } else {
774                    format!("module \"{module}\"").into()
775                }
776            }
777            Request::ServerRelative { path, .. } => format!("server relative {path}").into(),
778            Request::Windows { path, .. } => format!("windows {path}").into(),
779            Request::Empty => "empty".into(),
780            Request::PackageInternal { path } => format!("package internal {path}").into(),
781            Request::DataUri {
782                media_type,
783                encoding,
784                data,
785            } => format!(
786                "data uri \"{media_type}\" \"{encoding}\" \"{}\"",
787                data.await?
788            )
789            .into(),
790            Request::Uri {
791                protocol,
792                remainder,
793                ..
794            } => format!("uri \"{protocol}\" \"{remainder}\"").into(),
795            Request::Unknown { path } => format!("unknown {path}").into(),
796            Request::Dynamic => "dynamic".into(),
797            Request::Alternatives { requests } => {
798                let vec = requests.iter().map(|i| i.to_string()).try_join().await?;
799                vec.iter()
800                    .map(|r| r.as_str())
801                    .collect::<Vec<_>>()
802                    .join(" or ")
803                    .into()
804            }
805        }))
806    }
807}
808
809pub async fn stringify_data_uri(
810    media_type: &RcStr,
811    encoding: &RcStr,
812    data: ResolvedVc<RcStr>,
813) -> Result<String> {
814    Ok(format!(
815        "data:{media_type}{}{encoding},{}",
816        if encoding.is_empty() { "" } else { ";" },
817        data.await?
818    ))
819}