[red-knot] Add basic WASM API (#12654)

This commit is contained in:
Micha Reiser 2024-08-06 09:21:42 +02:00 committed by GitHub
parent f0318ff889
commit 10e977d5f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 473 additions and 31 deletions

View file

@ -111,7 +111,7 @@ jobs:
- name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- name: "Clippy (wasm)"
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
run: cargo clippy -p ruff_wasm -p red_knot_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
cargo-test-linux:
name: "cargo test (linux)"
@ -191,10 +191,14 @@ jobs:
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@v0.4.0
- uses: Swatinem/rust-cache@v2
- name: "Run wasm-pack"
- name: "Test ruff_wasm"
run: |
cd crates/ruff_wasm
wasm-pack test --node
- name: "Test red_knot_wasm"
run: |
cd crates/red_knot_wasm
wasm-pack test --node
cargo-build-release:
name: "cargo build (release)"

90
Cargo.lock generated
View file

@ -20,7 +20,7 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"getrandom",
"once_cell",
"version_check",
@ -270,6 +270,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -459,7 +465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
dependencies = [
"castaway",
"cfg-if",
"cfg-if 1.0.0",
"itoa",
"rustversion",
"ryu",
@ -486,7 +492,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"wasm-bindgen",
]
@ -523,7 +529,7 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@ -673,7 +679,7 @@ version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"hashbrown",
"lock_api",
"once_cell",
@ -686,7 +692,7 @@ version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"crossbeam-utils",
"hashbrown",
"lock_api",
@ -810,7 +816,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"home",
"windows-sys 0.48.0",
]
@ -836,7 +842,7 @@ version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"windows-sys 0.52.0",
@ -900,7 +906,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"js-sys",
"libc",
"wasi",
@ -932,7 +938,7 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"crunchy",
]
@ -1141,7 +1147,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@ -1390,6 +1396,12 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "mimalloc"
version = "0.1.43"
@ -1448,7 +1460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg-if 1.0.0",
"cfg_aliases",
"libc",
]
@ -1574,7 +1586,7 @@ version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
@ -1928,6 +1940,22 @@ dependencies = [
"tracing",
]
[[package]]
name = "red_knot_wasm"
version = "0.0.0"
dependencies = [
"console_error_panic_hook",
"console_log",
"js-sys",
"log",
"red_knot_workspace",
"ruff_db",
"ruff_notebook",
"wasm-bindgen",
"wasm-bindgen-test",
"wee_alloc",
]
[[package]]
name = "red_knot_workspace"
version = "0.0.0"
@ -2016,7 +2044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"cfg-if 1.0.0",
"getrandom",
"libc",
"spin",
@ -2134,6 +2162,7 @@ dependencies = [
"salsa",
"tempfile",
"tracing",
"web-time",
"zip",
]
@ -2989,7 +3018,7 @@ version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"fastrand",
"once_cell",
"rustix",
@ -3034,7 +3063,7 @@ version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"proc-macro2",
"quote",
"syn",
@ -3078,7 +3107,7 @@ version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"once_cell",
]
@ -3480,7 +3509,7 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
@ -3505,7 +3534,7 @@ version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"js-sys",
"wasm-bindgen",
"web-sys",
@ -3575,6 +3604,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.1"
@ -3584,6 +3623,18 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "which"
version = "6.0.1"
@ -3858,6 +3909,7 @@ dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
"flate2",
"zstd",
]

View file

@ -152,7 +152,7 @@ walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.92" }
wasm-bindgen-test = { version = "0.3.42" }
wild = { version = "2" }
zip = { version = "0.6.6", default-features = false, features = ["zstd"] }
zip = { version = "0.6.6", default-features = false }
[workspace.lints.rust]
unsafe_code = "warn"

View file

@ -25,7 +25,7 @@ zip = { workspace = true }
[build-dependencies]
path-slash = { workspace = true }
walkdir = { workspace = true }
zip = { workspace = true }
zip = { workspace = true, features = ["zstd", "deflate"] }
[dev-dependencies]
ruff_db = { workspace = true, features = ["os"] }

View file

@ -23,8 +23,21 @@ const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip";
fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
let mut zip = ZipWriter::new(writer);
// Use deflated compression for WASM builds because compiling `zstd-sys` requires clang
// [source](https://github.com/gyscos/zstd-rs/wiki/Compile-for-WASM) which complicates the build
// by a lot. Deflated compression is slower but it shouldn't matter much for the WASM use case
// (WASM itself is already slower than a native build for a specific platform).
// We can't use `#[cfg(...)]` here because the target-arch in a build script is the
// architecture of the system running the build script and not the architecture of the build-target.
// That's why we use the `TARGET` environment variable here.
let method = if std::env::var("TARGET").unwrap().contains("wasm32") {
CompressionMethod::Deflated
} else {
CompressionMethod::Zstd
};
let options = FileOptions::default()
.compression_method(CompressionMethod::Zstd)
.compression_method(method)
.unix_permissions(0o644);
for entry in walkdir::WalkDir::new(directory_path) {

View file

@ -0,0 +1,38 @@
[package]
name = "red_knot_wasm"
version = "0.0.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
description = "WebAssembly bindings for Red Knot"
[lib]
crate-type = ["cdylib", "rlib"]
doctest = false
[features]
default = ["console_error_panic_hook"]
[dependencies]
red_knot_workspace = { workspace = true }
ruff_db = { workspace = true }
ruff_notebook = { workspace = true }
console_error_panic_hook = { workspace = true, optional = true }
console_log = { workspace = true }
js-sys = { workspace = true }
log = { workspace = true }
wasm-bindgen = { workspace = true }
wee_alloc = "0.4.5"
[dev-dependencies]
wasm-bindgen-test = { workspace = true }
[lints]
workspace = true

View file

@ -0,0 +1,284 @@
use std::any::Any;
use js_sys::Error;
use wasm_bindgen::prelude::*;
use red_knot_workspace::db::RootDatabase;
use red_knot_workspace::workspace::WorkspaceMetadata;
use ruff_db::files::{system_path_to_file, File};
use ruff_db::program::{ProgramSettings, SearchPathSettings};
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
use ruff_db::system::{
DirectoryEntry, MemoryFileSystem, Metadata, System, SystemPath, SystemPathBuf,
SystemVirtualPath,
};
use ruff_notebook::Notebook;
// Use `wee_alloc` as the global allocator.
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen(start)]
pub fn run() {
use log::Level;
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong.");
}
#[wasm_bindgen]
pub struct Workspace {
db: RootDatabase,
system: WasmSystem,
}
#[wasm_bindgen]
impl Workspace {
#[wasm_bindgen(constructor)]
pub fn new(root: &str, settings: &Settings) -> Result<Workspace, Error> {
let system = WasmSystem::new(SystemPath::new(root));
let workspace =
WorkspaceMetadata::from_path(SystemPath::new(root), &system).map_err(into_error)?;
let program_settings = ProgramSettings {
target_version: settings.target_version.into(),
search_paths: SearchPathSettings::default(),
};
let db = RootDatabase::new(workspace, program_settings, system.clone());
Ok(Self { db, system })
}
#[wasm_bindgen(js_name = "openFile")]
pub fn open_file(&mut self, path: &str, contents: &str) -> Result<FileHandle, Error> {
self.system
.fs
.write_file(path, contents)
.map_err(into_error)?;
let file = system_path_to_file(&self.db, path).expect("File to exist");
file.sync(&mut self.db);
self.db.workspace().open_file(&mut self.db, file);
Ok(FileHandle {
file,
path: SystemPath::new(path).to_path_buf(),
})
}
#[wasm_bindgen(js_name = "updateFile")]
pub fn update_file(&mut self, file_id: &FileHandle, contents: &str) -> Result<(), Error> {
if !self.system.fs.exists(&file_id.path) {
return Err(Error::new("File does not exist"));
}
self.system
.fs
.write_file(&file_id.path, contents)
.map_err(into_error)?;
file_id.file.sync(&mut self.db);
Ok(())
}
#[wasm_bindgen(js_name = "closeFile")]
pub fn close_file(&mut self, file_id: &FileHandle) -> Result<(), Error> {
let file = file_id.file;
self.db.workspace().close_file(&mut self.db, file);
self.system
.fs
.remove_file(&file_id.path)
.map_err(into_error)?;
file.sync(&mut self.db);
Ok(())
}
/// Checks a single file.
#[wasm_bindgen(js_name = "checkFile")]
pub fn check_file(&self, file_id: &FileHandle) -> Result<Vec<String>, Error> {
let result = self.db.check_file(file_id.file).map_err(into_error)?;
Ok(result.to_vec())
}
/// Checks all open files
pub fn check(&self) -> Result<Vec<String>, Error> {
let result = self.db.check().map_err(into_error)?;
Ok(result.clone())
}
/// Returns the parsed AST for `path`
pub fn parsed(&self, file_id: &FileHandle) -> Result<String, Error> {
let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file);
Ok(format!("{:#?}", parsed.syntax()))
}
/// Returns the token stream for `path` serialized as a string.
pub fn tokens(&self, file_id: &FileHandle) -> Result<String, Error> {
let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file);
Ok(format!("{:#?}", parsed.tokens()))
}
#[wasm_bindgen(js_name = "sourceText")]
pub fn source_text(&self, file_id: &FileHandle) -> Result<String, Error> {
let source_text = ruff_db::source::source_text(&self.db, file_id.file);
Ok(source_text.to_string())
}
}
pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
Error::new(&err.to_string())
}
#[derive(Debug, Eq, PartialEq)]
#[wasm_bindgen(inspectable)]
pub struct FileHandle {
path: SystemPathBuf,
file: File,
}
#[wasm_bindgen]
impl FileHandle {
#[wasm_bindgen(js_name = toString)]
pub fn js_to_string(&self) -> String {
format!("file(id: {:?}, path: {})", self.file, self.path)
}
}
#[wasm_bindgen]
pub struct Settings {
pub target_version: TargetVersion,
}
#[wasm_bindgen]
impl Settings {
#[wasm_bindgen(constructor)]
pub fn new(target_version: TargetVersion) -> Self {
Self { target_version }
}
}
#[wasm_bindgen]
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum TargetVersion {
Py37,
#[default]
Py38,
Py39,
Py310,
Py311,
Py312,
Py313,
}
impl From<TargetVersion> for ruff_db::program::TargetVersion {
fn from(value: TargetVersion) -> Self {
match value {
TargetVersion::Py37 => Self::Py37,
TargetVersion::Py38 => Self::Py38,
TargetVersion::Py39 => Self::Py39,
TargetVersion::Py310 => Self::Py310,
TargetVersion::Py311 => Self::Py311,
TargetVersion::Py312 => Self::Py312,
TargetVersion::Py313 => Self::Py313,
}
}
}
#[derive(Debug, Clone)]
struct WasmSystem {
fs: MemoryFileSystem,
}
impl WasmSystem {
fn new(root: &SystemPath) -> Self {
Self {
fs: MemoryFileSystem::with_current_directory(root),
}
}
}
impl System for WasmSystem {
fn path_metadata(&self, path: &SystemPath) -> ruff_db::system::Result<Metadata> {
self.fs.metadata(path)
}
fn canonicalize_path(&self, path: &SystemPath) -> ruff_db::system::Result<SystemPathBuf> {
Ok(self.fs.canonicalize(path))
}
fn read_to_string(&self, path: &SystemPath) -> ruff_db::system::Result<String> {
self.fs.read_to_string(path)
}
fn read_to_notebook(
&self,
path: &SystemPath,
) -> Result<ruff_notebook::Notebook, ruff_notebook::NotebookError> {
let content = self.read_to_string(path)?;
Notebook::from_source_code(&content)
}
fn virtual_path_metadata(
&self,
_path: &SystemVirtualPath,
) -> ruff_db::system::Result<Metadata> {
Err(not_found())
}
fn read_virtual_path_to_string(
&self,
_path: &SystemVirtualPath,
) -> ruff_db::system::Result<String> {
Err(not_found())
}
fn read_virtual_path_to_notebook(
&self,
_path: &SystemVirtualPath,
) -> Result<ruff_notebook::Notebook, ruff_notebook::NotebookError> {
Err(ruff_notebook::NotebookError::Io(not_found()))
}
fn current_directory(&self) -> &SystemPath {
self.fs.current_directory()
}
fn read_directory<'a>(
&'a self,
path: &SystemPath,
) -> ruff_db::system::Result<
Box<dyn Iterator<Item = ruff_db::system::Result<DirectoryEntry>> + 'a>,
> {
Ok(Box::new(self.fs.read_directory(path)?))
}
fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
self.fs.walk_directory(path)
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn not_found() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory")
}

View file

@ -0,0 +1,21 @@
#![cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test;
use red_knot_wasm::{Settings, TargetVersion, Workspace};
#[wasm_bindgen_test]
fn check() {
let settings = Settings {
target_version: TargetVersion::Py312,
};
let mut workspace = Workspace::new("/", &settings).expect("Workspace to be created");
let test = workspace
.open_file("test.py", "import random22\n")
.expect("File to be opened");
let result = workspace.check_file(&test).expect("Check to succeed");
assert_eq!(result, vec!["Unresolved import 'random22'"]);
}

View file

@ -29,7 +29,13 @@ salsa = { workspace = true }
path-slash = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
zip = { workspace = true }
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
zip = { workspace = true, features = ["zstd"] }
[target.'cfg(target_arch="wasm32")'.dependencies]
web-time = { version = "1.1.0" }
zip = { workspace = true, features = ["deflate"] }
[dev-dependencies]
insta = { workspace = true }

View file

@ -77,7 +77,7 @@ impl std::fmt::Debug for TargetVersion {
}
/// Configures the search paths for module resolution.
#[derive(Eq, PartialEq, Debug, Clone)]
#[derive(Eq, PartialEq, Debug, Clone, Default)]
pub struct SearchPathSettings {
/// List of user-provided paths that should take first priority in the module resolution.
/// Examples in other type checkers are mypy's MYPYPATH environment variable,

View file

@ -213,7 +213,7 @@ impl MemoryFileSystem {
let file = get_or_create_file(&mut by_path, &normalized)?;
file.content = content.to_string();
file.last_modified = FileTime::now();
file.last_modified = now();
Ok(())
}
@ -229,7 +229,7 @@ impl MemoryFileSystem {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(File {
content: content.to_string(),
last_modified: FileTime::now(),
last_modified: now(),
});
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
@ -284,7 +284,7 @@ impl MemoryFileSystem {
let mut by_path = self.inner.by_path.write().unwrap();
let normalized = self.normalize_path(path.as_ref());
get_or_create_file(&mut by_path, &normalized)?.last_modified = FileTime::now();
get_or_create_file(&mut by_path, &normalized)?.last_modified = now();
Ok(())
}
@ -449,7 +449,7 @@ fn create_dir_all(
path.push(component);
let entry = paths.entry(path.clone()).or_insert_with(|| {
Entry::Directory(Directory {
last_modified: FileTime::now(),
last_modified: now(),
})
});
@ -472,7 +472,7 @@ fn get_or_create_file<'a>(
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
Entry::File(File {
content: String::new(),
last_modified: FileTime::now(),
last_modified: now(),
})
});
@ -654,6 +654,30 @@ enum WalkerState {
Nested { path: SystemPathBuf, depth: usize },
}
#[cfg(not(target_arch = "wasm32"))]
fn now() -> FileTime {
FileTime::now()
}
#[cfg(target_arch = "wasm32")]
fn now() -> FileTime {
// Copied from FileTime::from_system_time()
let time = web_time::SystemTime::now();
time.duration_since(web_time::UNIX_EPOCH)
.map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
.unwrap_or_else(|e| {
let until_epoch = e.duration();
let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
(0, 0)
} else {
(-1, 1_000_000_000 - until_epoch.subsec_nanos())
};
FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
})
}
#[cfg(test)]
mod tests {
use std::io::ErrorKind;