mirror of
https://github.com/aphrody-labs/deno.git
synced 2026-10-14 06:19:28 +00:00
In a `deno compile`-d binary, origin-bound storage did not work. The default `Deno.openKv()` (called without an explicit path) silently fell back to an in-memory database, so data never survived across runs, and both `localStorage` and the Web `caches` API threw `NotSupported`. The root cause was in the standalone runtime (`cli/rt/run.rs`), which built the worker factory with `StorageKeyResolver::empty()` and `origin_data_folder_path: None`. With no storage key the worker resolves both `origin_storage_dir` and `cache_storage_dir` to `None`; KV then opens an in-memory connection and the storage globals are disabled. Rather than route this through `DENO_DIR` the way `deno run` does, this treats a compiled binary as a standalone application. Persistent storage now lives under the platform's app data directory (`%LOCALAPPDATA%`, `~/Library/Application Support`, `$XDG_DATA_HOME`), reusing the same `get_data_local_dir()` resolution already used for self-extracting binaries, keyed by an app name. This keeps a shipped binary from writing user data into a Deno tooling cache that may not even exist on the target machine. The app name is the single source of storage identity. It is baked in at compile time from a new `--app-name` flag, falling back to the output file name when the flag is omitted, so it is stable across runs and even if the produced binary is later renamed. Recompiling with a different `--output`/`--app-name` starts a fresh store. Because the name (not the main-module URL) drives the directory, two binaries compiled with the same `--app-name` share a store while differently named apps stay isolated, and the previous footgun where two binaries that merely shared an executable file name would collide is gone. A single fixed storage key is used per app, so each app gets one store rather than per-origin namespacing; `--location` is no longer involved in storage. When the data directory cannot be resolved the storage key is left empty and the previous in-memory fallback is preserved, which also avoids unwrapping a missing data folder path in the worker. The app name is carried in the compile `Metadata` so the standalone runtime can read it back; `--app-version` (#29038) is a natural next member of this same app-identity metadata. Spec tests under `tests/specs/compile` compile a script and exercise it across cold processes: `kv_default_storage_path` proves a default `Deno.openKv()` survives between runs, `local_storage_default_path` does the same for `localStorage`, `web_cache_storage` proves `caches` is available, and `app_name_storage` proves two binaries sharing an `--app-name` share a store while a differently named app does not. Each test redirects the platform app data directory into its temp dir to stay hermetic. Fixes #24318
1269 lines
39 KiB
Rust
1269 lines
39 KiB
Rust
// Copyright 2018-2026 the Deno authors. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::collections::HashMap;
|
|
use std::ffi::OsString;
|
|
use std::io::ErrorKind;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::OnceLock;
|
|
|
|
use deno_core::FastString;
|
|
use deno_core::ModuleCodeBytes;
|
|
use deno_core::ModuleSourceCode;
|
|
use deno_core::ModuleType;
|
|
use deno_core::anyhow::Context;
|
|
use deno_core::anyhow::bail;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::serde_json;
|
|
use deno_core::url::Url;
|
|
use deno_error::JsError;
|
|
use deno_error::JsErrorBox;
|
|
use deno_lib::standalone::binary::DenoRtDeserializable;
|
|
use deno_lib::standalone::binary::MAGIC_BYTES;
|
|
use deno_lib::standalone::binary::Metadata;
|
|
use deno_lib::standalone::binary::RemoteModuleEntry;
|
|
use deno_lib::standalone::binary::SpecifierDataStore;
|
|
use deno_lib::standalone::binary::SpecifierId;
|
|
use deno_lib::standalone::virtual_fs::VfsEntry;
|
|
use deno_lib::standalone::virtual_fs::VirtualDirectory;
|
|
use deno_lib::standalone::virtual_fs::VirtualDirectoryEntries;
|
|
use deno_media_type::MediaType;
|
|
use deno_npm::NpmPackageId;
|
|
use deno_npm::resolution::SerializedNpmResolutionSnapshot;
|
|
use deno_npm::resolution::SerializedNpmResolutionSnapshotPackage;
|
|
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
|
|
use deno_semver::StackString;
|
|
use deno_semver::package::PackageReq;
|
|
use indexmap::IndexMap;
|
|
use sys_traits::FsCanonicalize;
|
|
use sys_traits::FsRead;
|
|
use thiserror::Error;
|
|
|
|
use crate::file_system::FileBackedVfs;
|
|
use crate::file_system::VfsRoot;
|
|
|
|
pub struct StandaloneData {
|
|
pub metadata: Metadata,
|
|
pub modules: Arc<StandaloneModules>,
|
|
pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>,
|
|
pub root_path: PathBuf,
|
|
pub vfs: Arc<FileBackedVfs>,
|
|
}
|
|
|
|
/// This function will try to run this binary as a standalone binary
|
|
/// produced by `deno compile`. It determines if this is a standalone
|
|
/// binary by skipping over the trailer width at the end of the file,
|
|
/// then checking for the magic trailer string `d3n0l4nd`. If found,
|
|
/// the bundle is executed. If not, this function exits with `Ok(None)`.
|
|
pub fn extract_standalone(
|
|
cli_args: Cow<[OsString]>,
|
|
) -> Result<StandaloneData, AnyError> {
|
|
extract_standalone_with_finder(cli_args, find_section)
|
|
}
|
|
|
|
/// Like `extract_standalone`, but allows providing a custom section finder.
|
|
/// This is used by the desktop cdylib to search its own image rather than
|
|
/// the main executable.
|
|
pub fn extract_standalone_with_finder(
|
|
cli_args: Cow<[OsString]>,
|
|
section_finder: fn() -> Result<&'static [u8], AnyError>,
|
|
) -> Result<StandaloneData, AnyError> {
|
|
let data = section_finder()?;
|
|
|
|
// read metadata first to determine the root path
|
|
let (mut metadata, remaining) = read_section_metadata(data)?;
|
|
|
|
// for self-extracting executables, use the extraction directory as root
|
|
// so that module specifiers resolve to extracted file paths
|
|
let root_path = if let Some(hash) = &metadata.self_extracting {
|
|
let dir = choose_and_create_extraction_dir(hash)?;
|
|
sys_traits::impls::RealSys
|
|
.fs_canonicalize(&dir)
|
|
.unwrap_or(dir)
|
|
} else {
|
|
let maybe_current_exe = std::env::current_exe().ok();
|
|
let current_exe_name = maybe_current_exe
|
|
.as_ref()
|
|
.and_then(|p| p.file_name())
|
|
.map(|p| p.to_string_lossy())
|
|
// should never happen
|
|
.unwrap_or_else(|| Cow::Borrowed("binary"));
|
|
std::env::temp_dir().join(format!("deno-compile-{}", current_exe_name))
|
|
};
|
|
let root_url = deno_path_util::url_from_directory_path(&root_path)?;
|
|
|
|
let DeserializedDataSection {
|
|
npm_snapshot,
|
|
modules_store: remote_modules,
|
|
vfs_root_entries,
|
|
vfs_files_data,
|
|
} = deserialize_binary_data_section(&root_url, remaining)?;
|
|
|
|
let cli_args = cli_args.into_owned();
|
|
let current_exe = std::env::current_exe().ok();
|
|
let mut args_iter = cli_args.into_iter();
|
|
args_iter.next(); // skip argv[0]
|
|
|
|
// Node.js apps relaunch with spawn(process.execPath, [process.argv[1], ...args]).
|
|
// In standalone mode process.argv[1] === execPath (#32990), so the first arg
|
|
// after argv[0] is a duplicate of the exe path. Strip it.
|
|
//
|
|
// NOTE: this means `./myapp ./myapp --foo` would silently lose the first
|
|
// `./myapp` arg. In practice standalone binaries are never invoked this way,
|
|
// and this matches how Node.js SEA handles the relaunch pattern.
|
|
if let Some(first) = args_iter.next() {
|
|
let is_exe_dup = current_exe
|
|
.as_ref()
|
|
.is_some_and(|exe| Path::new(&first) == exe.as_path());
|
|
if !is_exe_dup {
|
|
metadata.argv.push(first.into_string().unwrap());
|
|
}
|
|
}
|
|
for arg in args_iter {
|
|
metadata.argv.push(arg.into_string().unwrap());
|
|
}
|
|
let vfs = {
|
|
let fs_root = VfsRoot {
|
|
dir: VirtualDirectory {
|
|
// align the name of the directory with the root dir
|
|
name: root_path
|
|
.file_name()
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.into_owned(),
|
|
entries: vfs_root_entries,
|
|
},
|
|
root_path: root_path.clone(),
|
|
start_file_offset: 0,
|
|
};
|
|
Arc::new(FileBackedVfs::new(
|
|
Cow::Borrowed(vfs_files_data),
|
|
fs_root,
|
|
metadata.vfs_case_sensitivity,
|
|
))
|
|
};
|
|
Ok(StandaloneData {
|
|
metadata,
|
|
modules: Arc::new(StandaloneModules {
|
|
modules: remote_modules,
|
|
vfs: vfs.clone(),
|
|
}),
|
|
npm_snapshot,
|
|
root_path,
|
|
vfs,
|
|
})
|
|
}
|
|
|
|
/// Extracts the embedded file system to disk for a self-extracting
|
|
/// executable. The extraction_dir is the directory where files will be
|
|
/// extracted, which should match the VFS root_path.
|
|
pub fn extract_vfs_to_disk(
|
|
vfs: &FileBackedVfs,
|
|
extraction_dir: &Path,
|
|
) -> Result<(), AnyError> {
|
|
// check if already extracted
|
|
let done_marker = extraction_dir.join(".done");
|
|
if done_marker.exists() {
|
|
log::debug!("Already extracted to {}", extraction_dir.display());
|
|
return Ok(());
|
|
}
|
|
|
|
log::debug!("Extracting to {}", extraction_dir.display());
|
|
|
|
let start = std::time::Instant::now();
|
|
std::fs::create_dir_all(extraction_dir).with_context(|| {
|
|
format!(
|
|
"Failed to create extraction directory: {}",
|
|
extraction_dir.display()
|
|
)
|
|
})?;
|
|
extract_vfs_dir(
|
|
vfs,
|
|
vfs.root_dir(),
|
|
&mut extraction_dir.to_path_buf(),
|
|
extraction_dir,
|
|
)
|
|
.context("Failed to extract embedded files to disk")?;
|
|
|
|
// write the done marker
|
|
std::fs::File::create(&done_marker)
|
|
.context("Failed to write extraction done marker")?;
|
|
|
|
log::debug!("Extracted in {}ms", start.elapsed().as_millis());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn choose_and_create_extraction_dir(
|
|
hash_str: &str,
|
|
) -> Result<PathBuf, AnyError> {
|
|
let current_exe = std::env::current_exe()
|
|
.context("Failed to determine current executable path")?;
|
|
let exe_name = current_exe
|
|
.file_stem()
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
.unwrap_or_else(|| "binary".to_string());
|
|
|
|
// try next to the executable first
|
|
if let Some(exe_dir) = current_exe.parent() {
|
|
let dir = exe_dir.join(format!(".{}", exe_name)).join(hash_str);
|
|
match std::fs::create_dir_all(&dir) {
|
|
Ok(()) => return Ok(dir),
|
|
Err(err) => {
|
|
log::debug!(
|
|
"Could not create extraction dir next to executable ({}), falling back to data dir: {}",
|
|
err,
|
|
dir.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// fall back to platform-specific data directory
|
|
let data_dir = get_data_local_dir().context(
|
|
"Failed to determine local data directory for self-extracting executable",
|
|
)?;
|
|
let dir = data_dir.join(&exe_name).join(hash_str);
|
|
std::fs::create_dir_all(&dir).with_context(|| {
|
|
format!("Failed to create extraction directory: {}", dir.display())
|
|
})?;
|
|
Ok(dir)
|
|
}
|
|
|
|
pub(crate) fn get_data_local_dir() -> Option<PathBuf> {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
std::env::var_os("LOCALAPPDATA").map(PathBuf::from)
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
#[allow(clippy::disallowed_types, reason = "setup code")]
|
|
sys_traits::EnvHomeDir::env_home_dir(&sys_traits::impls::RealSys)
|
|
.map(|h| h.join("Library").join("Application Support"))
|
|
}
|
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
|
{
|
|
std::env::var_os("XDG_DATA_HOME")
|
|
.map(PathBuf::from)
|
|
.or_else(|| {
|
|
#[allow(clippy::disallowed_types, reason = "setup code")]
|
|
sys_traits::EnvHomeDir::env_home_dir(&sys_traits::impls::RealSys)
|
|
.map(|h| h.join(".local").join("share"))
|
|
})
|
|
}
|
|
}
|
|
|
|
fn extract_vfs_dir(
|
|
vfs: &FileBackedVfs,
|
|
dir: &VirtualDirectory,
|
|
disk_path: &mut PathBuf,
|
|
extraction_root: &Path,
|
|
) -> Result<(), AnyError> {
|
|
for entry in dir.entries.iter() {
|
|
match entry {
|
|
VfsEntry::Dir(sub_dir) => {
|
|
disk_path.push(&sub_dir.name);
|
|
// parent is guaranteed to exist since we recurse top-down
|
|
std::fs::create_dir(&*disk_path)
|
|
.or_else(|e| {
|
|
if e.kind() == std::io::ErrorKind::AlreadyExists {
|
|
Ok(())
|
|
} else {
|
|
Err(e)
|
|
}
|
|
})
|
|
.with_context(|| {
|
|
format!("Failed to create directory: {}", disk_path.display())
|
|
})?;
|
|
extract_vfs_dir(vfs, sub_dir, disk_path, extraction_root)?;
|
|
disk_path.pop();
|
|
}
|
|
VfsEntry::File(file) => {
|
|
disk_path.push(&file.name);
|
|
let data = vfs
|
|
.read_file_all(file)
|
|
.with_context(|| format!("Failed to read VFS file: {}", file.name))?;
|
|
#[cfg(unix)]
|
|
{
|
|
use std::io::Write;
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
let mode = if file.executable { 0o755 } else { 0o644 };
|
|
std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.mode(mode)
|
|
.open(&*disk_path)
|
|
.and_then(|mut f| f.write_all(&data))
|
|
.with_context(|| {
|
|
format!("Failed to write file: {}", disk_path.display())
|
|
})?;
|
|
}
|
|
#[cfg(not(unix))]
|
|
{
|
|
std::fs::write(&*disk_path, &*data).with_context(|| {
|
|
format!("Failed to write file: {}", disk_path.display())
|
|
})?;
|
|
}
|
|
disk_path.pop();
|
|
}
|
|
VfsEntry::Symlink(symlink) => {
|
|
disk_path.push(&symlink.name);
|
|
// symlink dest_parts are relative to the VFS root,
|
|
// so resolve them relative to the extraction root
|
|
let absolute_target = symlink.resolve_dest_from_root(extraction_root);
|
|
create_symlink(&absolute_target, disk_path, symlink.dest_is_dir)?;
|
|
disk_path.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn create_symlink(
|
|
target: &Path,
|
|
link_path: &Path,
|
|
dest_is_dir: bool,
|
|
) -> Result<(), AnyError> {
|
|
#[cfg(unix)]
|
|
{
|
|
_ = dest_is_dir; // unused on unix
|
|
std::os::unix::fs::symlink(target, link_path)
|
|
.or_else(|_| {
|
|
// may be left over from an interrupted extraction
|
|
let _ = std::fs::remove_file(link_path);
|
|
std::os::unix::fs::symlink(target, link_path)
|
|
})
|
|
.with_context(|| {
|
|
format!(
|
|
"Failed to create symlink: {} -> {}",
|
|
link_path.display(),
|
|
target.display()
|
|
)
|
|
})?;
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
// on Windows, symlink_file and symlink_dir are distinct — using the
|
|
// wrong type causes PermissionDenied when accessing through the link
|
|
let create_link = |target: &Path, link: &Path| {
|
|
if dest_is_dir {
|
|
std::os::windows::fs::symlink_dir(target, link)
|
|
} else {
|
|
std::os::windows::fs::symlink_file(target, link)
|
|
}
|
|
};
|
|
create_link(target, link_path)
|
|
.or_else(|_| {
|
|
// may be left over from an interrupted extraction
|
|
let _ = std::fs::remove_file(link_path)
|
|
.or_else(|_| std::fs::remove_dir(link_path));
|
|
create_link(target, link_path)
|
|
})
|
|
.or_else(|_| {
|
|
// symlinks may require elevated privileges on Windows,
|
|
// fall back to junctions for dirs or copying for files
|
|
if dest_is_dir {
|
|
junction::create(target, link_path)
|
|
} else {
|
|
std::fs::copy(target, link_path).map(|_| ())
|
|
}
|
|
})
|
|
.with_context(|| {
|
|
format!(
|
|
"Failed to create symlink: {} -> {}",
|
|
link_path.display(),
|
|
target.display()
|
|
)
|
|
})?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn find_section() -> Result<&'static [u8], AnyError> {
|
|
#[cfg(windows)]
|
|
if std::env::var_os("DENO_INTERNAL_RT_USE_FILE_FALLBACK").is_some() {
|
|
return read_from_file_fallback();
|
|
}
|
|
|
|
match libsui::find_section("d3n0l4nd")
|
|
.context("Failed reading standalone binary section.")
|
|
{
|
|
Ok(Some(data)) => Ok(data),
|
|
Ok(None) => bail!("Could not find standalone binary section."),
|
|
Err(err) => {
|
|
#[cfg(windows)]
|
|
if let Ok(data) = read_from_file_fallback() {
|
|
return Ok(data);
|
|
}
|
|
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is a temporary hacky fallback until we can find
|
|
/// a fix for https://github.com/denoland/deno/issues/28982
|
|
#[cfg(windows)]
|
|
fn read_from_file_fallback() -> Result<&'static [u8], AnyError> {
|
|
use std::sync::OnceLock;
|
|
|
|
fn find_in_bytes(bytes: &[u8], needle: &[u8]) -> Option<usize> {
|
|
bytes.windows(needle.len()).position(|n| n == needle)
|
|
}
|
|
|
|
static FILE: OnceLock<std::fs::File> = OnceLock::new();
|
|
static MMAP_FILE: OnceLock<memmap2::Mmap> = OnceLock::new();
|
|
|
|
// DENOLAND in utf16
|
|
const RESOURCE_SECTION_HEADER_NAME: &[u8] = &[
|
|
0x44, 0x00, 0x33, 0x00, 0x4E, 0x00, 0x30, 0x00, 0x4C, 0x00, 0x34, 0x00,
|
|
0x4E, 0x00, 0x44, 0x00,
|
|
];
|
|
const MAGIC_BYTES: &[u8] = b"d3n0l4nd";
|
|
|
|
let file_path = std::env::current_exe()?;
|
|
let file = FILE.get_or_init(|| std::fs::File::open(file_path).unwrap());
|
|
let mmap = MMAP_FILE.get_or_init(|| {
|
|
// SAFETY: memory mapped file creation
|
|
unsafe { memmap2::Mmap::map(file).unwrap() }
|
|
});
|
|
|
|
// the code in this file will cause this to appear twice in the binary,
|
|
// so skip over the first one
|
|
let Some(marker_pos) = find_in_bytes(mmap, RESOURCE_SECTION_HEADER_NAME)
|
|
else {
|
|
bail!("Failed to find first section name.");
|
|
};
|
|
let next_bytes = &mmap[marker_pos + RESOURCE_SECTION_HEADER_NAME.len()..];
|
|
let Some(marker_pos) =
|
|
find_in_bytes(next_bytes, RESOURCE_SECTION_HEADER_NAME)
|
|
else {
|
|
bail!("Failed to find second section name.");
|
|
};
|
|
let next_bytes =
|
|
&next_bytes[marker_pos + RESOURCE_SECTION_HEADER_NAME.len()..];
|
|
let Some(ascii_pos) = find_in_bytes(next_bytes, MAGIC_BYTES) else {
|
|
bail!("Failed to find first magic bytes.");
|
|
};
|
|
let next_bytes = &next_bytes[ascii_pos..];
|
|
let Some(last_pos) = next_bytes
|
|
.windows(MAGIC_BYTES.len())
|
|
.rposition(|w| w == MAGIC_BYTES)
|
|
else {
|
|
bail!("Failed to find end magic bytes.")
|
|
};
|
|
|
|
Ok(&next_bytes[..last_pos + MAGIC_BYTES.len()])
|
|
}
|
|
|
|
pub struct DeserializedDataSection {
|
|
pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>,
|
|
pub modules_store: RemoteModulesStore,
|
|
pub vfs_root_entries: VirtualDirectoryEntries,
|
|
pub vfs_files_data: &'static [u8],
|
|
}
|
|
|
|
fn read_magic_bytes(input: &[u8]) -> Result<(&[u8], bool), AnyError> {
|
|
if input.len() < MAGIC_BYTES.len() {
|
|
bail!("Unexpected end of data. Could not find magic bytes.");
|
|
}
|
|
let (magic_bytes, input) = input.split_at(MAGIC_BYTES.len());
|
|
if magic_bytes != MAGIC_BYTES {
|
|
return Ok((input, false));
|
|
}
|
|
Ok((input, true))
|
|
}
|
|
|
|
/// Reads the magic bytes and metadata from the beginning of the data section.
|
|
/// Returns the metadata and the remaining input after the metadata.
|
|
fn read_section_metadata(
|
|
data: &'static [u8],
|
|
) -> Result<(Metadata, &'static [u8]), AnyError> {
|
|
let (input, found) = read_magic_bytes(data)?;
|
|
if !found {
|
|
bail!("Did not find magic bytes.");
|
|
}
|
|
let (input, metadata_bytes) =
|
|
read_bytes_with_u64_len(input).context("reading metadata")?;
|
|
let metadata: Metadata =
|
|
serde_json::from_slice(metadata_bytes).context("deserializing metadata")?;
|
|
Ok((metadata, input))
|
|
}
|
|
|
|
/// Deserializes the binary data section after the metadata has already been
|
|
/// parsed by `read_section_metadata`.
|
|
fn deserialize_binary_data_section(
|
|
root_dir_url: &Url,
|
|
input: &'static [u8],
|
|
) -> Result<DeserializedDataSection, AnyError> {
|
|
// 1. Was the metadata above.
|
|
// 2. Npm snapshot
|
|
let (input, data) =
|
|
read_bytes_with_u64_len(input).context("reading npm snapshot")?;
|
|
let npm_snapshot = if data.is_empty() {
|
|
None
|
|
} else {
|
|
Some(deserialize_npm_snapshot(data).context("deserializing npm snapshot")?)
|
|
};
|
|
// 3. Specifiers
|
|
let (input, specifiers_store) =
|
|
SpecifierStore::deserialize(root_dir_url, input)
|
|
.context("deserializing specifiers")?;
|
|
// 4. Redirects
|
|
let (input, redirects_store) =
|
|
SpecifierDataStore::<SpecifierId>::deserialize(input)
|
|
.context("deserializing redirects")?;
|
|
// 5. Remote modules
|
|
let (input, remote_modules_store) =
|
|
SpecifierDataStore::<RemoteModuleEntry<'static>>::deserialize(input)
|
|
.context("deserializing remote modules")?;
|
|
// 6. VFS
|
|
let (input, data) = read_bytes_with_u64_len(input).context("vfs")?;
|
|
let vfs_root_entries: VirtualDirectoryEntries =
|
|
serde_json::from_slice(data).context("deserializing vfs data")?;
|
|
let (input, vfs_files_data) =
|
|
read_bytes_with_u64_len(input).context("reading vfs files data")?;
|
|
|
|
// finally ensure we read the magic bytes at the end
|
|
let (_input, found) = read_magic_bytes(input)?;
|
|
if !found {
|
|
bail!("Could not find magic bytes at end of data.");
|
|
}
|
|
|
|
let modules_store = RemoteModulesStore::new(
|
|
specifiers_store,
|
|
redirects_store,
|
|
remote_modules_store,
|
|
);
|
|
|
|
Ok(DeserializedDataSection {
|
|
npm_snapshot,
|
|
modules_store,
|
|
vfs_root_entries,
|
|
vfs_files_data,
|
|
})
|
|
}
|
|
|
|
struct SpecifierStore {
|
|
data: IndexMap<Arc<Url>, SpecifierId>,
|
|
reverse: IndexMap<SpecifierId, Arc<Url>>,
|
|
}
|
|
|
|
impl SpecifierStore {
|
|
pub fn deserialize<'a>(
|
|
root_dir_url: &Url,
|
|
input: &'a [u8],
|
|
) -> std::io::Result<(&'a [u8], Self)> {
|
|
let (input, len) = read_u32_as_usize(input)?;
|
|
let mut data = IndexMap::with_capacity(len);
|
|
let mut reverse = IndexMap::with_capacity(len);
|
|
let mut input = input;
|
|
for _ in 0..len {
|
|
let (new_input, specifier_str) = read_string_lossy(input)?;
|
|
let specifier = match Url::parse(&specifier_str) {
|
|
Ok(url) => url,
|
|
Err(err) => match root_dir_url.join(&specifier_str) {
|
|
Ok(url) => url,
|
|
Err(_) => {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
err,
|
|
));
|
|
}
|
|
},
|
|
};
|
|
let (new_input, id) = SpecifierId::deserialize(new_input)?;
|
|
let specifier = Arc::new(specifier);
|
|
data.insert(specifier.clone(), id);
|
|
reverse.insert(id, specifier);
|
|
input = new_input;
|
|
}
|
|
Ok((input, Self { data, reverse }))
|
|
}
|
|
|
|
pub fn get_id(&self, specifier: &Url) -> Option<SpecifierId> {
|
|
self.data.get(specifier).cloned()
|
|
}
|
|
|
|
pub fn get_specifier(&self, specifier_id: SpecifierId) -> Option<&Url> {
|
|
self.reverse.get(&specifier_id).map(|url| url.as_ref())
|
|
}
|
|
}
|
|
|
|
pub struct StandaloneModules {
|
|
modules: RemoteModulesStore,
|
|
vfs: Arc<FileBackedVfs>,
|
|
}
|
|
|
|
impl StandaloneModules {
|
|
pub fn resolve_specifier<'a>(
|
|
&'a self,
|
|
specifier: &'a Url,
|
|
) -> Result<Option<&'a Url>, TooManyRedirectsError> {
|
|
if specifier.scheme() == "file" {
|
|
Ok(Some(specifier))
|
|
} else {
|
|
self.modules.resolve_specifier(specifier)
|
|
}
|
|
}
|
|
|
|
pub fn path_in_root(&self, path: &Path) -> bool {
|
|
deno_path_util::normalize_path(Cow::Borrowed(path))
|
|
.starts_with(self.vfs.root())
|
|
}
|
|
|
|
pub fn read<'a>(
|
|
&'a self,
|
|
specifier: &'a Url,
|
|
) -> Result<Option<DenoCompileModuleData<'a>>, JsErrorBox> {
|
|
if specifier.scheme() == "file" {
|
|
let path = deno_path_util::url_to_file_path(specifier)
|
|
.map_err(JsErrorBox::from_err)?;
|
|
let mut transpiled = None;
|
|
let mut source_map = None;
|
|
let mut cjs_export_analysis = None;
|
|
let mut is_valid_utf8 = false;
|
|
let bytes = match self.vfs.file_entry(&path) {
|
|
Ok(entry) => {
|
|
let bytes = self
|
|
.vfs
|
|
.read_file_all(entry)
|
|
.map_err(JsErrorBox::from_err)?;
|
|
is_valid_utf8 = entry.is_valid_utf8;
|
|
transpiled = entry
|
|
.transpiled_offset
|
|
.and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
|
|
source_map = entry
|
|
.source_map_offset
|
|
.and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
|
|
cjs_export_analysis = entry
|
|
.cjs_export_analysis_offset
|
|
.and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
|
|
bytes
|
|
}
|
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
|
#[allow(
|
|
clippy::disallowed_types,
|
|
reason = "use real file system because not in vfs"
|
|
)]
|
|
let bytes = match sys_traits::impls::RealSys.fs_read(&path) {
|
|
Ok(bytes) => bytes,
|
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
|
return Ok(None);
|
|
}
|
|
Err(err) => return Err(JsErrorBox::from_err(err)),
|
|
};
|
|
// A file read from disk at runtime (e.g. a plugin discovered by a
|
|
// compiled program) hasn't been transpiled at compile time, so
|
|
// transpile TypeScript/JSX here, mirroring `deno run`.
|
|
let media_type = MediaType::from_specifier(specifier);
|
|
if matches!(
|
|
media_type,
|
|
MediaType::TypeScript
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Jsx
|
|
| MediaType::Tsx
|
|
) {
|
|
let text = String::from_utf8_lossy(&bytes).into_owned();
|
|
let transpiled_text =
|
|
transpile_runtime_module(specifier, media_type, text)?;
|
|
transpiled = Some(Cow::Owned(transpiled_text.into_bytes()));
|
|
}
|
|
bytes
|
|
}
|
|
Err(err) => return Err(JsErrorBox::from_err(err)),
|
|
};
|
|
Ok(Some(DenoCompileModuleData {
|
|
media_type: MediaType::from_specifier(specifier),
|
|
specifier,
|
|
is_valid_utf8,
|
|
data: bytes,
|
|
transpiled,
|
|
source_map,
|
|
cjs_export_analysis,
|
|
}))
|
|
} else {
|
|
self.modules.read(specifier).map_err(JsErrorBox::from_err)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Transpile TypeScript/JSX source to JavaScript for modules that are not
|
|
/// embedded in the binary, namely `data:`/`blob:` URLs and local files read
|
|
/// from disk at runtime. Embedded modules are already transpiled at
|
|
/// `deno compile` time, but a compiled program can still dynamically `import()`
|
|
/// TypeScript that it builds or discovers while running. `deno_ast` is already
|
|
/// linked into the binary via `ext/node`, so transpiling here adds no extra
|
|
/// binary size.
|
|
///
|
|
/// JSX configuration (`jsx`, `jsxImportSource`, `jsxFactory`, etc.) is
|
|
/// discovered by walking up from the source file to find the user's
|
|
/// `deno.json` / `deno.jsonc` once and caching the result. Without this,
|
|
/// every `.tsx` from a Preact-based project would be emitted as
|
|
/// `React.createElement(...)` and fail at runtime with `React is not defined`.
|
|
pub(crate) fn transpile_runtime_module(
|
|
specifier: &Url,
|
|
media_type: MediaType,
|
|
source: String,
|
|
) -> Result<String, JsErrorBox> {
|
|
match media_type {
|
|
MediaType::TypeScript
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Jsx
|
|
| MediaType::Tsx => {}
|
|
// JavaScript, JSON, Wasm, etc. are served verbatim.
|
|
_ => return Ok(source),
|
|
}
|
|
|
|
let parsed = deno_ast::parse_module(deno_ast::ParseParams {
|
|
specifier: specifier.clone(),
|
|
text: source.into(),
|
|
media_type,
|
|
capture_tokens: false,
|
|
scope_analysis: false,
|
|
maybe_syntax: None,
|
|
})
|
|
.map_err(JsErrorBox::from_err)?;
|
|
let opts = runtime_transpile_options(specifier);
|
|
let transpiled = parsed
|
|
.transpile(
|
|
opts,
|
|
&deno_ast::TranspileModuleOptions { module_kind: None },
|
|
&deno_ast::EmitOptions {
|
|
source_map: deno_ast::SourceMapOption::Inline,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.map_err(JsErrorBox::from_err)?;
|
|
Ok(transpiled.into_source().text)
|
|
}
|
|
|
|
/// Resolved JSX/decorator options for runtime transpiles, discovered lazily
|
|
/// from the user's project `deno.json[c]` and cached for the lifetime of the
|
|
/// process.
|
|
static RUNTIME_TRANSPILE_OPTIONS: OnceLock<deno_ast::TranspileOptions> =
|
|
OnceLock::new();
|
|
|
|
/// Walk up from `specifier`'s parent directory looking for `deno.json` or
|
|
/// `deno.jsonc`, parse `compilerOptions.jsx` + related, and return a matching
|
|
/// `TranspileOptions`. Falls back to defaults (with `ImportsNotUsedAsValues::Remove`)
|
|
/// if no config is found or parsing fails — those are non-fatal here.
|
|
fn runtime_transpile_options(
|
|
specifier: &Url,
|
|
) -> &'static deno_ast::TranspileOptions {
|
|
RUNTIME_TRANSPILE_OPTIONS.get_or_init(|| {
|
|
let fallback = deno_ast::TranspileOptions {
|
|
imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove,
|
|
..Default::default()
|
|
};
|
|
let Ok(path) = deno_path_util::url_to_file_path(specifier) else {
|
|
return fallback;
|
|
};
|
|
let Some(mut dir) = path.parent().map(|p| p.to_path_buf()) else {
|
|
return fallback;
|
|
};
|
|
let cfg_path = 'outer: loop {
|
|
for name in ["deno.json", "deno.jsonc"] {
|
|
let candidate = dir.join(name);
|
|
if candidate.exists() {
|
|
break 'outer Some(candidate);
|
|
}
|
|
}
|
|
if !dir.pop() {
|
|
break 'outer None;
|
|
}
|
|
};
|
|
let Some(cfg_path) = cfg_path else {
|
|
return fallback;
|
|
};
|
|
let Ok(text) = std::fs::read_to_string(&cfg_path) else {
|
|
return fallback;
|
|
};
|
|
let Some(json) =
|
|
jsonc_parser::parse_to_serde_value(&text, &Default::default())
|
|
.ok()
|
|
.flatten()
|
|
else {
|
|
return fallback;
|
|
};
|
|
let json: &serde_json::Value = &json;
|
|
transpile_options_from_compiler_options(
|
|
json.get("compilerOptions"),
|
|
&fallback,
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Lower the relevant subset of `compilerOptions` (the same one
|
|
/// `cli/tools/pack/mod.rs::create_transpile_options` handles) into
|
|
/// `TranspileOptions`. Anything missing falls back to `fallback`.
|
|
fn transpile_options_from_compiler_options(
|
|
compiler_options: Option<&serde_json::Value>,
|
|
fallback: &deno_ast::TranspileOptions,
|
|
) -> deno_ast::TranspileOptions {
|
|
let get_str = |key: &str| -> Option<String> {
|
|
compiler_options?.get(key)?.as_str().map(|s| s.to_string())
|
|
};
|
|
let get_bool = |key: &str| -> bool {
|
|
compiler_options
|
|
.and_then(|opts| opts.get(key))
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false)
|
|
};
|
|
let jsx = get_str("jsx");
|
|
let jsx_import_source = get_str("jsxImportSource");
|
|
let jsx_factory = get_str("jsxFactory");
|
|
let jsx_fragment_factory = get_str("jsxFragmentFactory");
|
|
let jsx_runtime = match jsx.as_deref() {
|
|
Some("react") => {
|
|
Some(deno_ast::JsxRuntime::Classic(deno_ast::JsxClassicOptions {
|
|
factory: jsx_factory
|
|
.unwrap_or_else(|| "React.createElement".to_string()),
|
|
fragment_factory: jsx_fragment_factory
|
|
.unwrap_or_else(|| "React.Fragment".to_string()),
|
|
}))
|
|
}
|
|
Some("react-jsx") => Some(deno_ast::JsxRuntime::Automatic(
|
|
deno_ast::JsxAutomaticOptions {
|
|
development: false,
|
|
import_source: jsx_import_source,
|
|
},
|
|
)),
|
|
Some("react-jsxdev") => Some(deno_ast::JsxRuntime::Automatic(
|
|
deno_ast::JsxAutomaticOptions {
|
|
development: true,
|
|
import_source: jsx_import_source,
|
|
},
|
|
)),
|
|
Some("precompile") => Some(deno_ast::JsxRuntime::Precompile(
|
|
deno_ast::JsxPrecompileOptions {
|
|
automatic: deno_ast::JsxAutomaticOptions {
|
|
development: false,
|
|
import_source: jsx_import_source,
|
|
},
|
|
skip_elements: None,
|
|
dynamic_props: None,
|
|
},
|
|
)),
|
|
_ => fallback.jsx.clone(),
|
|
};
|
|
let experimental_decorators = get_bool("experimentalDecorators");
|
|
let emit_decorator_metadata = get_bool("emitDecoratorMetadata");
|
|
deno_ast::TranspileOptions {
|
|
jsx: jsx_runtime,
|
|
decorators: if experimental_decorators {
|
|
deno_ast::DecoratorsTranspileOption::LegacyTypeScript {
|
|
emit_metadata: emit_decorator_metadata,
|
|
}
|
|
} else {
|
|
deno_ast::DecoratorsTranspileOption::Ecma
|
|
},
|
|
imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove,
|
|
var_decl_imports: false,
|
|
verbatim_module_syntax: false,
|
|
}
|
|
}
|
|
|
|
pub struct DenoCompileModuleData<'a> {
|
|
pub specifier: &'a Url,
|
|
pub media_type: MediaType,
|
|
pub is_valid_utf8: bool,
|
|
pub data: Cow<'static, [u8]>,
|
|
pub transpiled: Option<Cow<'static, [u8]>>,
|
|
pub source_map: Option<Cow<'static, [u8]>>,
|
|
pub cjs_export_analysis: Option<Cow<'static, [u8]>>,
|
|
}
|
|
|
|
impl<'a> DenoCompileModuleData<'a> {
|
|
pub fn into_parts(self) -> (&'a Url, ModuleType, DenoCompileModuleSource) {
|
|
fn into_string_unsafe(
|
|
is_valid_utf8: bool,
|
|
data: Cow<'static, [u8]>,
|
|
) -> DenoCompileModuleSource {
|
|
match data {
|
|
Cow::Borrowed(d) if is_valid_utf8 => {
|
|
DenoCompileModuleSource::String(
|
|
// SAFETY: we know this is a valid utf8 string
|
|
unsafe { std::str::from_utf8_unchecked(d) },
|
|
)
|
|
}
|
|
Cow::Borrowed(_) => DenoCompileModuleSource::Bytes(data),
|
|
Cow::Owned(d) => DenoCompileModuleSource::Bytes(Cow::Owned(d)),
|
|
}
|
|
}
|
|
|
|
let data = self.transpiled.unwrap_or(self.data);
|
|
let (media_type, source) = match self.media_type {
|
|
MediaType::JavaScript
|
|
| MediaType::Jsx
|
|
| MediaType::Mjs
|
|
| MediaType::Cjs
|
|
| MediaType::TypeScript
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Dts
|
|
| MediaType::Dmts
|
|
| MediaType::Dcts
|
|
| MediaType::Tsx => (
|
|
ModuleType::JavaScript,
|
|
into_string_unsafe(self.is_valid_utf8, data),
|
|
),
|
|
MediaType::Json => (
|
|
ModuleType::Json,
|
|
into_string_unsafe(self.is_valid_utf8, data),
|
|
),
|
|
MediaType::Wasm => {
|
|
(ModuleType::Wasm, DenoCompileModuleSource::Bytes(data))
|
|
}
|
|
// just assume javascript if we made it here
|
|
MediaType::Css
|
|
| MediaType::Html
|
|
| MediaType::Jsonc
|
|
| MediaType::Json5
|
|
| MediaType::Markdown
|
|
| MediaType::SourceMap
|
|
| MediaType::Sql
|
|
| MediaType::Unknown => {
|
|
(ModuleType::JavaScript, DenoCompileModuleSource::Bytes(data))
|
|
}
|
|
};
|
|
(self.specifier, media_type, source)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum DenoCompileModuleSource {
|
|
String(&'static str),
|
|
Bytes(Cow<'static, [u8]>),
|
|
}
|
|
|
|
impl DenoCompileModuleSource {
|
|
pub fn into_for_v8(self) -> ModuleSourceCode {
|
|
match self {
|
|
// todo(https://github.com/denoland/deno_core/pull/943): store whether
|
|
// the string is ascii or not ahead of time so we can avoid the is_ascii()
|
|
// check in FastString::from_static
|
|
Self::String(s) => ModuleSourceCode::String(FastString::from_static(s)),
|
|
Self::Bytes(b) => ModuleSourceCode::Bytes(module_source_into_bytes(b)),
|
|
}
|
|
}
|
|
|
|
pub fn into_bytes_for_v8(self) -> ModuleCodeBytes {
|
|
match self {
|
|
DenoCompileModuleSource::String(text) => text.as_bytes().into(),
|
|
DenoCompileModuleSource::Bytes(b) => module_source_into_bytes(b),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn module_source_into_bytes(data: Cow<'static, [u8]>) -> ModuleCodeBytes {
|
|
match data {
|
|
Cow::Borrowed(d) => d.into(),
|
|
Cow::Owned(d) => d.into_boxed_slice().into(),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
#[class(generic)]
|
|
#[error("Too many redirects resolving: {0}")]
|
|
pub struct TooManyRedirectsError(Url);
|
|
|
|
pub struct RemoteModulesStore {
|
|
specifiers: SpecifierStore,
|
|
redirects: SpecifierDataStore<SpecifierId>,
|
|
remote_modules: SpecifierDataStore<RemoteModuleEntry<'static>>,
|
|
}
|
|
|
|
impl RemoteModulesStore {
|
|
fn new(
|
|
specifiers: SpecifierStore,
|
|
redirects: SpecifierDataStore<SpecifierId>,
|
|
remote_modules: SpecifierDataStore<RemoteModuleEntry<'static>>,
|
|
) -> Self {
|
|
Self {
|
|
specifiers,
|
|
redirects,
|
|
remote_modules,
|
|
}
|
|
}
|
|
|
|
pub fn resolve_specifier<'a>(
|
|
&'a self,
|
|
specifier: &'a Url,
|
|
) -> Result<Option<&'a Url>, TooManyRedirectsError> {
|
|
let Some(mut current) = self.specifiers.get_id(specifier) else {
|
|
return Ok(None);
|
|
};
|
|
let mut count = 0;
|
|
loop {
|
|
if count > 10 {
|
|
return Err(TooManyRedirectsError(specifier.clone()));
|
|
}
|
|
match self.redirects.get(current) {
|
|
Some(to) => {
|
|
current = *to;
|
|
count += 1;
|
|
}
|
|
None => {
|
|
if count == 0 {
|
|
return Ok(Some(specifier));
|
|
} else {
|
|
return Ok(self.specifiers.get_specifier(current));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn read<'a>(
|
|
&'a self,
|
|
original_specifier: &'a Url,
|
|
) -> Result<Option<DenoCompileModuleData<'a>>, TooManyRedirectsError> {
|
|
#[allow(clippy::ptr_arg, reason = "Cow on data is relevant here")]
|
|
fn handle_cow_ref(data: &Cow<'static, [u8]>) -> Cow<'static, [u8]> {
|
|
match data {
|
|
Cow::Borrowed(data) => Cow::Borrowed(data),
|
|
Cow::Owned(data) => {
|
|
// this variant should never happen because the data
|
|
// should always be borrowed static in denort
|
|
debug_assert!(false);
|
|
Cow::Owned(data.clone())
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut count = 0;
|
|
let Some(mut specifier) = self.specifiers.get_id(original_specifier) else {
|
|
return Ok(None);
|
|
};
|
|
loop {
|
|
if count > 10 {
|
|
return Err(TooManyRedirectsError(original_specifier.clone()));
|
|
}
|
|
match self.redirects.get(specifier) {
|
|
Some(to) => {
|
|
specifier = *to;
|
|
count += 1;
|
|
}
|
|
None => {
|
|
let Some(entry) = self.remote_modules.get(specifier) else {
|
|
return Ok(None);
|
|
};
|
|
return Ok(Some(DenoCompileModuleData {
|
|
specifier: if count == 0 {
|
|
original_specifier
|
|
} else {
|
|
self.specifiers.get_specifier(specifier).unwrap()
|
|
},
|
|
media_type: entry.media_type,
|
|
is_valid_utf8: entry.is_valid_utf8,
|
|
data: handle_cow_ref(&entry.data),
|
|
transpiled: entry.maybe_transpiled.as_ref().map(handle_cow_ref),
|
|
source_map: entry.maybe_source_map.as_ref().map(handle_cow_ref),
|
|
cjs_export_analysis: entry
|
|
.maybe_cjs_export_analysis
|
|
.as_ref()
|
|
.map(handle_cow_ref),
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn deserialize_npm_snapshot(
|
|
input: &[u8],
|
|
) -> Result<ValidSerializedNpmResolutionSnapshot, AnyError> {
|
|
fn parse_id(input: &[u8]) -> Result<(&[u8], NpmPackageId), AnyError> {
|
|
let (input, id) = read_string_lossy(input)?;
|
|
let id = NpmPackageId::from_serialized(&id)?;
|
|
Ok((input, id))
|
|
}
|
|
|
|
#[allow(clippy::needless_lifetimes, reason = "clippy bug")]
|
|
#[allow(clippy::type_complexity, reason = "private code")]
|
|
fn parse_root_package<'a>(
|
|
id_to_npm_id: &'a impl Fn(usize) -> Result<NpmPackageId, AnyError>,
|
|
) -> impl Fn(&[u8]) -> Result<(&[u8], (PackageReq, NpmPackageId)), AnyError> + 'a
|
|
{
|
|
|input| {
|
|
let (input, req) = read_string_lossy(input)?;
|
|
let req = PackageReq::from_str(&req)?;
|
|
let (input, id) = read_u32_as_usize(input)?;
|
|
Ok((input, (req, id_to_npm_id(id)?)))
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::needless_lifetimes, reason = "clippy bug")]
|
|
#[allow(clippy::type_complexity, reason = "private code")]
|
|
fn parse_package_dep<'a>(
|
|
id_to_npm_id: &'a impl Fn(usize) -> Result<NpmPackageId, AnyError>,
|
|
) -> impl Fn(&[u8]) -> Result<(&[u8], (StackString, NpmPackageId)), AnyError> + 'a
|
|
{
|
|
|input| {
|
|
let (input, req) = read_string_lossy(input)?;
|
|
let (input, id) = read_u32_as_usize(input)?;
|
|
let req = StackString::from_cow(req);
|
|
Ok((input, (req, id_to_npm_id(id)?)))
|
|
}
|
|
}
|
|
|
|
fn parse_package<'a>(
|
|
input: &'a [u8],
|
|
id: NpmPackageId,
|
|
id_to_npm_id: &impl Fn(usize) -> Result<NpmPackageId, AnyError>,
|
|
) -> Result<(&'a [u8], SerializedNpmResolutionSnapshotPackage), AnyError> {
|
|
let (input, deps_len) = read_u32_as_usize(input)?;
|
|
let (input, dependencies) =
|
|
parse_hashmap_n_times(input, deps_len, parse_package_dep(id_to_npm_id))?;
|
|
Ok((
|
|
input,
|
|
SerializedNpmResolutionSnapshotPackage {
|
|
id,
|
|
system: Default::default(),
|
|
dist: Default::default(),
|
|
dependencies,
|
|
optional_dependencies: Default::default(),
|
|
optional_peer_dependencies: Default::default(),
|
|
has_bin: false,
|
|
has_scripts: false,
|
|
is_deprecated: false,
|
|
extra: Default::default(),
|
|
},
|
|
))
|
|
}
|
|
|
|
let (input, packages_len) = read_u32_as_usize(input)?;
|
|
|
|
// get a hashmap of all the npm package ids to their serialized ids
|
|
let (input, data_ids_to_npm_ids) =
|
|
parse_vec_n_times(input, packages_len, parse_id)
|
|
.context("deserializing id")?;
|
|
let data_id_to_npm_id = |id: usize| {
|
|
data_ids_to_npm_ids
|
|
.get(id)
|
|
.cloned()
|
|
.ok_or_else(|| deno_core::anyhow::anyhow!("Invalid npm package id"))
|
|
};
|
|
|
|
let (input, root_packages_len) = read_u32_as_usize(input)?;
|
|
let (input, root_packages) = parse_hashmap_n_times(
|
|
input,
|
|
root_packages_len,
|
|
parse_root_package(&data_id_to_npm_id),
|
|
)
|
|
.context("deserializing root package")?;
|
|
let (input, packages) =
|
|
parse_vec_n_times_with_index(input, packages_len, |input, index| {
|
|
parse_package(input, data_id_to_npm_id(index)?, &data_id_to_npm_id)
|
|
})
|
|
.context("deserializing package")?;
|
|
|
|
if !input.is_empty() {
|
|
bail!("Unexpected data left over");
|
|
}
|
|
|
|
Ok(
|
|
SerializedNpmResolutionSnapshot {
|
|
packages,
|
|
root_packages,
|
|
}
|
|
// this is ok because we have already verified that all the
|
|
// identifiers found in the snapshot are valid via the
|
|
// npm package id -> npm package id mapping
|
|
.into_valid_unsafe(),
|
|
)
|
|
}
|
|
|
|
fn parse_hashmap_n_times<TKey: std::cmp::Eq + std::hash::Hash, TValue>(
|
|
mut input: &[u8],
|
|
times: usize,
|
|
parse: impl Fn(&[u8]) -> Result<(&[u8], (TKey, TValue)), AnyError>,
|
|
) -> Result<(&[u8], HashMap<TKey, TValue>), AnyError> {
|
|
let mut results = HashMap::with_capacity(times);
|
|
for _ in 0..times {
|
|
let result = parse(input);
|
|
let (new_input, (key, value)) = result?;
|
|
results.insert(key, value);
|
|
input = new_input;
|
|
}
|
|
Ok((input, results))
|
|
}
|
|
|
|
fn parse_vec_n_times<TResult>(
|
|
input: &[u8],
|
|
times: usize,
|
|
parse: impl Fn(&[u8]) -> Result<(&[u8], TResult), AnyError>,
|
|
) -> Result<(&[u8], Vec<TResult>), AnyError> {
|
|
parse_vec_n_times_with_index(input, times, |input, _index| parse(input))
|
|
}
|
|
|
|
fn parse_vec_n_times_with_index<TResult>(
|
|
mut input: &[u8],
|
|
times: usize,
|
|
parse: impl Fn(&[u8], usize) -> Result<(&[u8], TResult), AnyError>,
|
|
) -> Result<(&[u8], Vec<TResult>), AnyError> {
|
|
let mut results = Vec::with_capacity(times);
|
|
for i in 0..times {
|
|
let result = parse(input, i);
|
|
let (new_input, result) = result?;
|
|
results.push(result);
|
|
input = new_input;
|
|
}
|
|
Ok((input, results))
|
|
}
|
|
|
|
fn read_bytes_with_u64_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> {
|
|
let (input, len) = read_u64(input)?;
|
|
let (input, data) = read_bytes(input, len as usize)?;
|
|
Ok((input, data))
|
|
}
|
|
|
|
fn read_bytes_with_u32_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> {
|
|
let (input, len) = read_u32_as_usize(input)?;
|
|
let (input, data) = read_bytes(input, len)?;
|
|
Ok((input, data))
|
|
}
|
|
|
|
fn read_bytes(input: &[u8], len: usize) -> std::io::Result<(&[u8], &[u8])> {
|
|
check_has_len(input, len)?;
|
|
let (len_bytes, input) = input.split_at(len);
|
|
Ok((input, len_bytes))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn check_has_len(input: &[u8], len: usize) -> std::io::Result<()> {
|
|
if input.len() < len {
|
|
Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"Unexpected end of data",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn read_string_lossy(input: &[u8]) -> std::io::Result<(&[u8], Cow<'_, str>)> {
|
|
let (input, data_bytes) = read_bytes_with_u32_len(input)?;
|
|
Ok((input, String::from_utf8_lossy(data_bytes)))
|
|
}
|
|
|
|
fn read_u32_as_usize(input: &[u8]) -> std::io::Result<(&[u8], usize)> {
|
|
let (input, len_bytes) = read_bytes(input, 4)?;
|
|
let len = u32::from_le_bytes(len_bytes.try_into().unwrap());
|
|
Ok((input, len as usize))
|
|
}
|
|
|
|
fn read_u64(input: &[u8]) -> std::io::Result<(&[u8], u64)> {
|
|
let (input, len_bytes) = read_bytes(input, 8)?;
|
|
let len = u64::from_le_bytes(len_bytes.try_into().unwrap());
|
|
Ok((input, len))
|
|
}
|