deno_core/
fast_string.rs

1// Copyright 2018-2025 the Deno authors. MIT license.
2
3use serde::Deserializer;
4use serde::Serializer;
5use std::borrow::Borrow;
6use std::ffi::OsStr;
7use std::fmt::Debug;
8use std::fmt::Display;
9use std::fmt::Formatter;
10use std::hash::Hash;
11use std::ops::Deref;
12use std::sync::Arc;
13use url::Url;
14use v8::NewStringType;
15
16use crate::ToV8;
17
18static EMPTY_STRING: v8::OneByteConst =
19  v8::String::create_external_onebyte_const("".as_bytes());
20
21/// A static string that is compile-time checked to be ASCII and is stored in the
22/// most efficient possible way to create V8 strings.
23#[derive(Clone, Copy)]
24#[repr(transparent)]
25pub struct FastStaticString {
26  s: &'static v8::OneByteConst,
27}
28
29impl FastStaticString {
30  pub const fn new(s: &'static v8::OneByteConst) -> Self {
31    FastStaticString { s }
32  }
33
34  pub fn as_str(&self) -> &'static str {
35    self.s.as_ref()
36  }
37
38  pub fn as_bytes(&self) -> &'static [u8] {
39    self.s.as_ref()
40  }
41
42  #[doc(hidden)]
43  pub const fn create_external_onebyte_const(
44    s: &'static [u8],
45  ) -> v8::OneByteConst {
46    v8::String::create_external_onebyte_const(s)
47  }
48
49  pub fn v8_string<'s, 'i>(
50    &self,
51    scope: &mut v8::PinScope<'s, 'i>,
52  ) -> Result<v8::Local<'s, v8::String>, FastStringV8AllocationError> {
53    FastString::from(*self).v8_string(scope)
54  }
55
56  pub const fn into_v8_const_ptr(&self) -> *const v8::OneByteConst {
57    self.s as _
58  }
59}
60
61impl From<&'static v8::OneByteConst> for FastStaticString {
62  fn from(s: &'static v8::OneByteConst) -> Self {
63    Self::new(s)
64  }
65}
66
67impl From<FastStaticString> for *const v8::OneByteConst {
68  fn from(val: FastStaticString) -> Self {
69    val.into_v8_const_ptr()
70  }
71}
72
73impl Hash for FastStaticString {
74  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
75    self.as_str().hash(state)
76  }
77}
78
79impl AsRef<str> for FastStaticString {
80  fn as_ref(&self) -> &str {
81    self.as_str()
82  }
83}
84
85impl Deref for FastStaticString {
86  type Target = str;
87  fn deref(&self) -> &Self::Target {
88    self.as_str()
89  }
90}
91
92impl Borrow<str> for FastStaticString {
93  fn borrow(&self) -> &str {
94    self.as_str()
95  }
96}
97
98impl Debug for FastStaticString {
99  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100    Debug::fmt(self.as_str(), f)
101  }
102}
103
104impl Default for FastStaticString {
105  fn default() -> Self {
106    FastStaticString { s: &EMPTY_STRING }
107  }
108}
109
110impl PartialEq for FastStaticString {
111  fn eq(&self, other: &Self) -> bool {
112    self.as_bytes() == other.as_bytes()
113  }
114}
115
116impl Eq for FastStaticString {}
117
118impl Display for FastStaticString {
119  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
120    f.write_str(self.as_str())
121  }
122}
123
124#[derive(Debug, deno_error::JsError)]
125#[class(type)]
126pub struct FastStringV8AllocationError;
127
128impl std::error::Error for FastStringV8AllocationError {}
129impl std::fmt::Display for FastStringV8AllocationError {
130  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
131    write!(
132      f,
133      "failed to allocate string; buffer exceeds maximum length"
134    )
135  }
136}
137
138/// Module names and code can be sourced from strings or bytes that are either owned or borrowed. This enumeration allows us
139/// to perform a minimal amount of cloning and format-shifting of the underlying data.
140///
141/// Note that any [`FastString`] created using [`ascii_str!`] must contain only ASCII characters. Other [`FastString`] types
142/// may be UTF-8, though this will incur a small performance penalty. It is recommended that large, static strings always
143/// use [`ascii_str!`].
144///
145/// Examples of ways to construct a [`FastString`]:
146///
147/// ```rust
148/// # use deno_core::{ascii_str, FastString};
149///
150/// let code: FastString = ascii_str!("a string").into();
151/// let code: FastString = format!("a string").into();
152/// ```
153pub struct FastString {
154  inner: FastStringInner,
155}
156
157enum FastStringInner {
158  /// Created from static data.
159  Static(&'static str),
160
161  /// Created from static ascii, known to contain only ASCII chars.
162  StaticAscii(&'static str),
163
164  /// Created from static data, known to contain only ASCII chars.
165  StaticConst(FastStaticString),
166
167  /// An owned chunk of data. Note that we use `Box` rather than `Vec` to avoid the
168  /// storage overhead.
169  Owned(Box<str>),
170
171  // Scripts loaded from the `deno_graph` infrastructure.
172  Arc(Arc<str>),
173}
174
175impl FastString {
176  /// Create a [`FastString`] from a static string. The string may contain
177  /// non-ASCII characters, and if so, will take the slower path when used
178  /// in v8.
179  pub const fn from_static(s: &'static str) -> Self {
180    if s.is_ascii() {
181      Self {
182        inner: FastStringInner::StaticAscii(s),
183      }
184    } else {
185      Self {
186        inner: FastStringInner::Static(s),
187      }
188    }
189  }
190
191  /// Create a [`FastString`] from a static string that is known to contain
192  /// only ASCII characters.
193  ///
194  /// Note: This function is deliberately not `const fn`. Use `from_static`
195  /// in const contexts.
196  ///
197  /// # Safety
198  ///
199  /// It is unsafe to specify a non-ASCII string here because this will be
200  /// referenced in an external one byte static string in v8, which requires
201  /// the data be Latin-1 or ASCII.
202  ///
203  /// This should only be used in scenarios where you know a string is ASCII
204  /// and you want to avoid the performance overhead of checking if a string
205  /// is ASCII that `from_static` does.
206  pub unsafe fn from_ascii_static_unchecked(s: &'static str) -> Self {
207    debug_assert!(
208      s.is_ascii(),
209      "use `from_non_ascii_static_unsafe` for non-ASCII strings",
210    );
211    Self {
212      inner: FastStringInner::StaticAscii(s),
213    }
214  }
215
216  /// Create a [`FastString`] from a static string that may contain non-ASCII
217  /// characters.
218  ///
219  /// This should only be used in scenarios where you know a string is not ASCII
220  /// and you want to avoid the performance overhead of checking if the string
221  /// is ASCII that `from_static` does.
222  ///
223  /// Note: This function is deliberately not `const fn`. Use `from_static`
224  /// in const contexts. This function is not unsafe because using this with
225  /// an ascii string will just not be as optimal for performance.
226  pub fn from_non_ascii_static(s: &'static str) -> Self {
227    Self {
228      inner: FastStringInner::Static(s),
229    }
230  }
231
232  /// Returns a static string from this `FastString`, if available.
233  pub fn as_static_str(&self) -> Option<&'static str> {
234    match self.inner {
235      FastStringInner::Static(s) => Some(s),
236      FastStringInner::StaticAscii(s) => Some(s),
237      FastStringInner::StaticConst(s) => Some(s.as_str()),
238      _ => None,
239    }
240  }
241
242  /// Creates a cheap copy of this [`FastString`], potentially transmuting it
243  /// to a faster form. Note that this is not a clone operation as it consumes
244  /// the old [`FastString`].
245  pub fn into_cheap_copy(self) -> (Self, Self) {
246    match self.inner {
247      FastStringInner::Owned(s) => {
248        let s: Arc<str> = s.into();
249        (
250          Self {
251            inner: FastStringInner::Arc(s.clone()),
252          },
253          Self {
254            inner: FastStringInner::Arc(s),
255          },
256        )
257      }
258      _ => (self.try_clone().unwrap(), self),
259    }
260  }
261
262  /// If this [`FastString`] is cheaply cloneable, returns a clone.
263  pub fn try_clone(&self) -> Option<Self> {
264    match &self.inner {
265      FastStringInner::Static(s) => Some(Self {
266        inner: FastStringInner::Static(s),
267      }),
268      FastStringInner::StaticAscii(s) => Some(Self {
269        inner: FastStringInner::StaticAscii(s),
270      }),
271      FastStringInner::StaticConst(s) => Some(Self {
272        inner: FastStringInner::StaticConst(*s),
273      }),
274      FastStringInner::Arc(s) => Some(Self {
275        inner: FastStringInner::Arc(s.clone()),
276      }),
277      FastStringInner::Owned(_s) => None,
278    }
279  }
280
281  #[inline(always)]
282  pub fn as_bytes(&self) -> &[u8] {
283    self.as_str().as_bytes()
284  }
285
286  #[inline(always)]
287  pub fn as_str(&self) -> &str {
288    match &self.inner {
289      // TODO(mmastrac): When we get a const deref, as_str can be const
290      FastStringInner::Arc(s) => s,
291      FastStringInner::Owned(s) => s,
292      FastStringInner::Static(s) => s,
293      FastStringInner::StaticAscii(s) => s,
294      FastStringInner::StaticConst(s) => s.as_str(),
295    }
296  }
297
298  /// Create a v8 string from this [`FastString`]. If the string is static and contains only ASCII characters,
299  /// an external one-byte static is created.
300  pub fn v8_string<'a, 'i>(
301    &self,
302    scope: &mut v8::PinScope<'a, 'i>,
303  ) -> Result<v8::Local<'a, v8::String>, FastStringV8AllocationError> {
304    match self.inner {
305      FastStringInner::StaticAscii(s) => {
306        v8::String::new_external_onebyte_static(scope, s.as_bytes())
307          .ok_or(FastStringV8AllocationError)
308      }
309      FastStringInner::StaticConst(s) => {
310        v8::String::new_from_onebyte_const(scope, s.s)
311          .ok_or(FastStringV8AllocationError)
312      }
313      _ => {
314        v8::String::new_from_utf8(scope, self.as_bytes(), NewStringType::Normal)
315          .ok_or(FastStringV8AllocationError)
316      }
317    }
318  }
319
320  /// Truncates a [`FastString`] value, possibly re-allocating or memcpy'ing. May be slow.
321  pub fn truncate(&mut self, index: usize) {
322    match &mut self.inner {
323      FastStringInner::Static(b) => {
324        self.inner = FastStringInner::Static(&b[..index])
325      }
326      FastStringInner::StaticAscii(b) => {
327        self.inner = FastStringInner::StaticAscii(&b[..index])
328      }
329      FastStringInner::StaticConst(b) => {
330        self.inner = FastStringInner::StaticAscii(&b.as_str()[..index])
331      }
332      // TODO(mmastrac): this could be more efficient
333      FastStringInner::Owned(b) => {
334        self.inner = FastStringInner::Owned(b[..index].to_owned().into())
335      }
336      // We can't do much if we have an Arc<str>, so we'll just take ownership of the truncated version
337      FastStringInner::Arc(s) => {
338        self.inner = FastStringInner::Arc(s[..index].to_owned().into())
339      }
340    }
341  }
342}
343
344impl Hash for FastString {
345  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
346    self.as_str().hash(state)
347  }
348}
349
350impl AsRef<str> for FastString {
351  fn as_ref(&self) -> &str {
352    self.as_str()
353  }
354}
355
356impl AsRef<[u8]> for FastString {
357  fn as_ref(&self) -> &[u8] {
358    self.as_str().as_ref()
359  }
360}
361
362impl AsRef<OsStr> for FastString {
363  fn as_ref(&self) -> &OsStr {
364    self.as_str().as_ref()
365  }
366}
367
368impl Deref for FastString {
369  type Target = str;
370  fn deref(&self) -> &Self::Target {
371    self.as_str()
372  }
373}
374
375impl Borrow<str> for FastString {
376  fn borrow(&self) -> &str {
377    self.as_str()
378  }
379}
380
381impl Debug for FastString {
382  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383    Debug::fmt(self.as_str(), f)
384  }
385}
386
387impl Default for FastString {
388  fn default() -> Self {
389    Self {
390      inner: FastStringInner::StaticConst(FastStaticString::default()),
391    }
392  }
393}
394
395impl PartialEq for FastString {
396  fn eq(&self, other: &Self) -> bool {
397    self.as_bytes() == other.as_bytes()
398  }
399}
400
401impl Eq for FastString {}
402
403impl Display for FastString {
404  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
405    f.write_str(self.as_str())
406  }
407}
408
409/// [`FastString`] can be made cheaply from [`Url`] as we know it's owned and don't need to do an
410/// ASCII check.
411impl From<FastStaticString> for FastString {
412  fn from(value: FastStaticString) -> Self {
413    Self {
414      inner: FastStringInner::StaticConst(value),
415    }
416  }
417}
418
419/// [`FastString`] can be made cheaply from [`Url`] as we know it's owned and don't need to do an
420/// ASCII check.
421impl From<Url> for FastString {
422  fn from(value: Url) -> Self {
423    let s: String = value.into();
424    s.into()
425  }
426}
427
428/// [`FastString`] can be made cheaply from [`String`] as we know it's owned and don't need to do an
429/// ASCII check.
430impl From<String> for FastString {
431  fn from(value: String) -> Self {
432    Self {
433      inner: FastStringInner::Owned(value.into_boxed_str()),
434    }
435  }
436}
437
438/// [`FastString`] can be made cheaply from [`Arc<str>`] as we know it's shared and don't need to do an
439/// ASCII check.
440impl From<Arc<str>> for FastString {
441  fn from(value: Arc<str>) -> Self {
442    Self {
443      inner: FastStringInner::Arc(value),
444    }
445  }
446}
447
448impl From<FastString> for Arc<str> {
449  fn from(value: FastString) -> Self {
450    use FastStringInner::*;
451    match value.inner {
452      Static(text) | StaticAscii(text) => text.into(),
453      StaticConst(text) => text.as_ref().into(),
454      Owned(text) => text.into(),
455      Arc(text) => text,
456    }
457  }
458}
459
460impl<'s> ToV8<'s> for FastString {
461  type Error = FastStringV8AllocationError;
462
463  #[inline]
464  fn to_v8<'i>(
465    self,
466    scope: &mut v8::PinScope<'s, 'i>,
467  ) -> Result<v8::Local<'s, v8::Value>, Self::Error> {
468    Ok(self.v8_string(scope)?.into())
469  }
470}
471
472impl serde::Serialize for FastString {
473  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
474  where
475    S: Serializer,
476  {
477    serializer.serialize_str(self.as_str())
478  }
479}
480
481type DeserializeProxy<'de> = &'de str;
482
483impl<'de> serde::Deserialize<'de> for FastString {
484  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
485  where
486    D: Deserializer<'de>,
487  {
488    DeserializeProxy::<'de>::deserialize(deserializer)
489      .map(|v| v.to_owned().into())
490  }
491}
492
493/// Include a fast string in the binary. This string is asserted at compile-time to be 7-bit ASCII for optimal
494/// v8 performance.
495///
496/// This macro creates a [`FastStaticString`] that may be converted to a [`FastString`] via [`Into::into`].
497#[macro_export]
498macro_rules! ascii_str_include {
499  ($file:expr_2021) => {{
500    const STR: $crate::v8::OneByteConst =
501      $crate::FastStaticString::create_external_onebyte_const(
502        ::std::include_str!($file).as_bytes(),
503      );
504    let s: &'static $crate::v8::OneByteConst = &STR;
505    $crate::FastStaticString::new(s)
506  }};
507}
508
509/// Include a fast string in the binary from a string literal. This string is asserted at compile-time to be
510/// 7-bit ASCII for optimal v8 performance.
511///
512/// This macro creates a [`FastStaticString`] that may be converted to a [`FastString`] via [`Into::into`].
513#[macro_export]
514macro_rules! ascii_str {
515  ($str:expr_2021) => {{
516    const C: $crate::v8::OneByteConst =
517      $crate::FastStaticString::create_external_onebyte_const($str.as_bytes());
518    $crate::FastStaticString::new(&C)
519  }};
520}
521
522/// Used to generate the fast, const versions of op names. Internal only.
523#[macro_export]
524#[doc(hidden)]
525macro_rules! __op_name_fast {
526  ($op:ident) => {{
527    const LITERAL: &'static [u8] = stringify!($op).as_bytes();
528    const STR: $crate::v8::OneByteConst =
529      $crate::FastStaticString::create_external_onebyte_const(LITERAL);
530    let s: &'static $crate::v8::OneByteConst = &STR;
531    (stringify!($op), $crate::FastStaticString::new(s))
532  }};
533}
534
535#[cfg(test)]
536mod tests {
537  use super::*;
538
539  #[test]
540  fn string_eq() {
541    let s: FastString = ascii_str!("Testing").into();
542    assert_eq!("Testing", s.as_str());
543    let s2 = FastString::from_static("Testing");
544    assert_eq!(s, s2);
545    let (s1, s2) = s.into_cheap_copy();
546    assert_eq!("Testing", s1.as_str());
547    assert_eq!("Testing", s2.as_str());
548
549    let s = FastString::from("Testing".to_owned());
550    assert_eq!("Testing", s.as_str());
551    let (s1, s2) = s.into_cheap_copy();
552    assert_eq!("Testing", s1.as_str());
553    assert_eq!("Testing", s2.as_str());
554  }
555
556  #[test]
557  fn truncate() {
558    let mut s = "123456".to_owned();
559    s.truncate(3);
560
561    let mut code: FastString = ascii_str!("123456").into();
562    code.truncate(3);
563    assert_eq!(s, code.as_str());
564
565    let mut code: FastString = "123456".to_owned().into();
566    code.truncate(3);
567    assert_eq!(s, code.as_str());
568
569    let arc_str: Arc<str> = "123456".into();
570    let mut code: FastString = arc_str.into();
571    code.truncate(3);
572    assert_eq!(s, code.as_str());
573  }
574
575  #[test]
576  fn test_large_include() {
577    // This test would require an excessively large file in the repo, so we just run this manually
578    // ascii_str_include!("runtime/tests/large_string.txt");
579    // ascii_str_include!(concat!("runtime", "/tests/", "large_string.txt"));
580  }
581
582  /// Ensure that all of our macros compile properly in a static context.
583  #[test]
584  fn test_const() {
585    const _: (&str, FastStaticString) = __op_name_fast!(op_name);
586    const _: FastStaticString = ascii_str!("hmm");
587    const _: FastStaticString = ascii_str!(concat!("hmm", "hmmmmm"));
588    const _: FastStaticString = ascii_str_include!("Cargo.toml");
589    const _: FastStaticString = ascii_str_include!(concat!("./", "Cargo.toml"));
590  }
591}