denort_helper/
lib.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::borrow::Cow;
4use std::fs::File;
5use std::hash::Hash;
6use std::hash::Hasher;
7use std::io::BufReader;
8use std::io::Read;
9use std::path::Path;
10use std::path::PathBuf;
11use std::sync::Arc;
12
13#[derive(Debug, thiserror::Error, deno_error::JsError)]
14pub enum LoadError {
15  #[class(generic)]
16  #[error("Failed to write native addon (Deno FFI/Node API) '{0}' to '{1}' because the file system was readonly. This is a limitation of native addons with deno compile.", executable_path.display(), real_path.display())]
17  ReadOnlyFilesystem {
18    real_path: PathBuf,
19    executable_path: PathBuf,
20  },
21  #[class(generic)]
22  #[error("Failed to write native addon (Deno FFI/Node API) '{0}' to '{1}'.", executable_path.display(), real_path.display())]
23  FailedWriting {
24    real_path: PathBuf,
25    executable_path: PathBuf,
26    #[source]
27    source: std::io::Error,
28  },
29}
30
31pub type DenoRtNativeAddonLoaderRc = Arc<dyn DenoRtNativeAddonLoader>;
32
33/// Loads native addons in `deno compile`.
34///
35/// The implementation should provide the bytes from the binary
36/// of the native file.
37pub trait DenoRtNativeAddonLoader: Send + Sync {
38  fn load_if_in_vfs(&self, path: &Path) -> Option<Cow<'static, [u8]>>;
39
40  fn load_and_resolve_path<'a>(
41    &self,
42    path: &'a Path,
43  ) -> Result<Cow<'a, Path>, LoadError> {
44    match self.load_if_in_vfs(path) {
45      Some(bytes) => {
46        let exe_name = std::env::current_exe().ok();
47        let exe_name = exe_name
48          .as_ref()
49          .and_then(|p| p.file_stem())
50          .map(|s| s.to_string_lossy())
51          .unwrap_or("denort".into());
52        let real_path = resolve_temp_file_name(&exe_name, path, &bytes);
53        if let Err(err) = deno_path_util::fs::atomic_write_file(
54          &sys_traits::impls::RealSys,
55          &real_path,
56          &bytes,
57          0o644,
58        ) {
59          if err.kind() == std::io::ErrorKind::ReadOnlyFilesystem {
60            return Err(LoadError::ReadOnlyFilesystem {
61              real_path,
62              executable_path: path.to_path_buf(),
63            });
64          }
65
66          // another process might be using it... so only surface
67          // the error if the files aren't equivalent
68          if !file_matches_bytes(&real_path, &bytes) {
69            return Err(LoadError::FailedWriting {
70              executable_path: path.to_path_buf(),
71              real_path,
72              source: err,
73            });
74          }
75        }
76        Ok(Cow::Owned(real_path))
77      }
78      None => Ok(Cow::Borrowed(path)),
79    }
80  }
81}
82
83fn file_matches_bytes(path: &Path, expected_bytes: &[u8]) -> bool {
84  let file = match File::open(path) {
85    Ok(f) => f,
86    Err(_) => return false,
87  };
88  let len_on_disk = match file.metadata() {
89    Ok(m) => m.len(),
90    Err(_) => return false,
91  };
92  if len_on_disk as usize != expected_bytes.len() {
93    return false; // bail early
94  }
95
96  // Stream‑compare in fixed‑size chunks.
97  const CHUNK: usize = 8 * 1024;
98  let mut reader = BufReader::with_capacity(CHUNK, file);
99  let mut buf = [0u8; CHUNK];
100  let mut offset = 0;
101
102  loop {
103    match reader.read(&mut buf) {
104      Ok(0) => return offset == expected_bytes.len(),
105      Ok(n) => {
106        let next_offset = offset + n;
107        if next_offset > expected_bytes.len()
108          || buf[..n] != expected_bytes[offset..next_offset]
109        {
110          return false;
111        }
112        offset = next_offset;
113      }
114      Err(_) => return false,
115    }
116  }
117}
118
119fn resolve_temp_file_name(
120  current_exe_name: &str,
121  path: &Path,
122  bytes: &[u8],
123) -> PathBuf {
124  // should be deterministic
125  let path_hash = {
126    let mut hasher = twox_hash::XxHash64::default();
127    path.hash(&mut hasher);
128    hasher.finish()
129  };
130  let bytes_hash = {
131    let mut hasher = twox_hash::XxHash64::default();
132    bytes.hash(&mut hasher);
133    hasher.finish()
134  };
135  let mut file_name =
136    format!("{}{}{}", current_exe_name, path_hash, bytes_hash);
137  if let Some(ext) = path.extension() {
138    file_name.push('.');
139    file_name.push_str(&ext.to_string_lossy());
140  }
141  std::env::temp_dir().join(&file_name)
142}
143
144#[cfg(test)]
145mod test {
146  use super::*;
147
148  #[test]
149  fn test_file_matches_bytes() {
150    let tempdir = tempfile::TempDir::new().unwrap();
151    let path = tempdir.path().join("file.txt");
152    let mut bytes = vec![0u8; 17892];
153    for (i, byte) in bytes.iter_mut().enumerate() {
154      *byte = i as u8;
155    }
156    std::fs::write(&path, &bytes).unwrap();
157    assert!(file_matches_bytes(&path, &bytes));
158    bytes[17192] = 9;
159    assert!(!file_matches_bytes(&path, &bytes));
160  }
161
162  #[test]
163  fn test_resolve_temp_file_name() {
164    let file_path = PathBuf::from("/test/test.node");
165    let bytes: [u8; 3] = [1, 2, 3];
166    let temp_file = resolve_temp_file_name("exe_name", &file_path, &bytes);
167    assert_eq!(
168      temp_file,
169      std::env::temp_dir()
170        .join("exe_name1805603793990095570513255480333703631005.node")
171    );
172  }
173}