char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somdeslop: consolidate public type and registry test utils7325e8c

main
35.2 KiB1021 linesraw
1mod links;
2mod paths;
3mod posix;
4mod watch;
5mod workflows;
6
7use std::collections::{HashMap, VecDeque};
8use std::convert::Infallible;
9use std::ffi::OsString;
10use std::io::{self, Read, Seek, SeekFrom, Write};
11use std::path::{Component, Path, PathBuf};
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::sync::{Arc, Mutex};
14
15use avril_caps::{Capabilities, PathTarget};
16use avril_core::serde_v8::{JsBuffer, ToJsBuffer};
17use avril_core::{Extension, Op, PublicModule};
18use avril_host::io::{CHUNK, MAX_SLICES, Response, response};
19use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt, OpenOptionsSyncExt};
20use cap_std::fs::{Dir, File, Metadata, OpenOptions};
21use futures::channel::oneshot;
22use tokio::runtime::Handle;
23
24pub const PUBLIC_MODULES: &[PublicModule] = &[
25    avril_core::public_module!("avril:fs", "ext:file", "fs"),
26    avril_core::public_module!("avril:fs/sync", "ext:file", "fs_sync"),
27];
28
29const MAX_ID: u64 = 9_007_199_254_740_991;
30const SAFE_INTEGER: u64 = (1 << 53) - 1;
31/// Runs against the file and returns the reply's delivery.
32type Operation = Box<dyn FnOnce(&mut Option<Opened>, bool) -> Delivery + Send>;
33type Delivery = Box<dyn FnOnce() + Send>;
34type Reply<T> = oneshot::Receiver<io::Result<T>>;
35type Options = (bool, bool, bool, bool, bool, bool, Option<u32>);
36type Info = (
37    bool,
38    bool,
39    bool,
40    u64,
41    Option<f64>,
42    Option<f64>,
43    Option<f64>,
44    Option<u32>,
45    Option<u32>,
46    Option<u32>,
47);
48
49async fn receive<T>(reply: Reply<T>) -> Response<T> {
50    response(
51        reply
52            .await
53            .unwrap_or_else(|_| Err(io::Error::other("Filesystem worker stopped"))),
54    )
55}
56
57fn failed<T>(error: io::Error) -> Reply<T> {
58    let (reply, receive) = oneshot::channel();
59    let _ = reply.send(Err(error));
60    receive
61}
62
63fn filesystem_path(caps: &Capabilities, label: &str) -> io::Result<PathBuf> {
64    #[cfg(windows)]
65    if !Path::new(label).has_root()
66        && matches!(
67            Path::new(label).components().next(),
68            Some(Component::Prefix(_))
69        )
70    {
71        return Err(io::Error::new(
72            io::ErrorKind::Unsupported,
73            "not yet implemented: drive-relative filesystem paths",
74        ));
75    }
76    Ok(caps.cwd().join(label))
77}
78
79fn denied(label: &str, write: bool) -> io::Error {
80    io::Error::new(
81        io::ErrorKind::PermissionDenied,
82        format!("{} {label}", if write { "write" } else { "read" }),
83    )
84}
85
86fn authorize(caps: &Capabilities, target: &PathTarget, label: &str, write: bool) -> io::Result<()> {
87    if write {
88        caps.check_write(target)
89    } else {
90        caps.check_read(target)
91    }
92    .map_err(|_| denied(label, write))
93}
94
95fn bad_resource() -> io::Error {
96    io::Error::new(
97        io::ErrorKind::NotConnected,
98        "File handle is closed or does not allow this operation",
99    )
100}
101
102#[derive(Clone)]
103struct BlockingPool {
104    runtime: Handle,
105}
106
107impl BlockingPool {
108    fn run<T: Send + 'static>(
109        &self,
110        work: impl FnOnce() -> io::Result<T> + Send + 'static,
111    ) -> Reply<T> {
112        let (reply, receive) = oneshot::channel();
113        self.runtime.spawn_blocking(move || {
114            let _ = reply.send(work());
115        });
116        receive
117    }
118}
119
120struct Queue {
121    operations: VecDeque<Operation>,
122    running: bool,
123}
124
125struct Resource {
126    closed: Arc<AtomicBool>,
127    queue: Mutex<Queue>,
128    file: Mutex<Option<Opened>>,
129    pool: BlockingPool,
130}
131
132impl Resource {
133    fn submit<T: Send + 'static>(
134        self: &Arc<Self>,
135        operation: impl FnOnce(&mut Opened) -> io::Result<T> + Send + 'static,
136    ) -> Reply<T> {
137        let (reply, receive) = oneshot::channel();
138        let mut queue = self.queue.lock().unwrap();
139        if self.closed.load(Ordering::Acquire) {
140            return failed(bad_resource());
141        }
142        queue.operations.push_back(Box::new(move |file, closed| {
143            let result = if closed {
144                Err(bad_resource())
145            } else {
146                file.as_mut().ok_or_else(bad_resource).and_then(operation)
147            };
148            Box::new(move || {
149                let _ = reply.send(result);
150            })
151        }));
152        if !queue.running {
153            queue.running = true;
154            let resource = self.clone();
155            self.pool.runtime.spawn_blocking(move || resource.run());
156        }
157        receive
158    }
159
160    /// Allows multi-batch operations to observe closure without releasing the queue.
161    fn submit_observed<T: Send + 'static>(
162        self: &Arc<Self>,
163        operation: impl FnOnce(&mut Opened, &AtomicBool) -> io::Result<T> + Send + 'static,
164    ) -> Reply<T> {
165        let closed = self.closed.clone();
166        self.submit(move |opened| operation(opened, &closed))
167    }
168
169    fn run(self: Arc<Self>) {
170        loop {
171            let operation = {
172                let mut queue = self.queue.lock().unwrap();
173                match queue.operations.pop_front() {
174                    Some(operation) => operation,
175                    None => {
176                        queue.running = false;
177                        drop(queue);
178                        return self.finish();
179                    }
180                }
181            };
182            let delivery = {
183                let mut file = self.file.lock().unwrap();
184                let closed = self.closed.load(Ordering::Acquire);
185                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
186                    operation(&mut file, closed)
187                }))
188                .inspect_err(|_| {
189                    self.closed.store(true, Ordering::Release);
190                    file.take();
191                })
192                .ok()
193            };
194            // Settle the queue before waking the caller, so code resuming on
195            // the reply (such as lending the handle to a subprocess) finds
196            // the handle idle rather than busy. Delivering under the queue
197            // lock keeps replies in submission order.
198            let idle = {
199                let mut queue = self.queue.lock().unwrap();
200                queue.running = !queue.operations.is_empty();
201                if let Some(delivery) = delivery {
202                    delivery();
203                }
204                !queue.running
205            };
206            if idle {
207                return self.finish();
208            }
209        }
210    }
211
212    fn finish(&self) {
213        if self.closed.load(Ordering::Acquire) {
214            self.file.lock().unwrap().take();
215        }
216    }
217
218    fn close(self: &Arc<Self>) {
219        self.closed.store(true, Ordering::Release);
220        let mut queue = self.queue.lock().unwrap();
221        if !queue.running {
222            queue.running = true;
223            let resource = self.clone();
224            // Physical closure stays off V8's thread, including finalizer cleanup.
225            self.pool.runtime.spawn_blocking(move || resource.run());
226        }
227    }
228}
229
230struct Opened {
231    file: File,
232    target: PathTarget,
233    label: String,
234    caps: Arc<Capabilities>,
235    read: bool,
236    write: bool,
237}
238
239impl Opened {
240    fn check(&self, write: bool) -> io::Result<()> {
241        if (write && !self.write) || (!write && !self.read) {
242            return Err(bad_resource());
243        }
244        authorize(&self.caps, &self.target, &self.label, write)
245    }
246
247    fn check_metadata(&self) -> io::Result<()> {
248        if self.read {
249            let read = self.check(false);
250            if read.is_ok() || !self.write {
251                return read;
252            }
253        }
254        self.check(true)
255    }
256}
257
258fn open_file(
259    caps: Arc<Capabilities>,
260    path: String,
261    options: Options,
262    non_regular: io::ErrorKind,
263) -> io::Result<Opened> {
264    if path.is_empty() {
265        return Err(io::Error::new(
266            io::ErrorKind::NotFound,
267            "Empty filesystem path",
268        ));
269    }
270    let (read, write, append, truncate, create, create_new, mode) = options;
271    if (!read && !write && !append)
272        || ((truncate || create || create_new) && !write && !append)
273        || mode.is_some_and(|mode| mode > 0o7777)
274    {
275        return Err(io::Error::new(
276            io::ErrorKind::InvalidInput,
277            "Invalid open options",
278        ));
279    }
280    let write = write || append;
281    let absolute = filesystem_path(&caps, &path)?;
282    let target = match if create_new {
283        PathTarget::for_entry(absolute)
284    } else {
285        PathTarget::for_creation(absolute)
286    } {
287        Ok(target) => target,
288        Err(_) if read && !caps.unrestricted_read() => {
289            return Err(denied(&path, false));
290        }
291        Err(_) if write && !caps.unrestricted_write() => {
292            return Err(denied(&path, true));
293        }
294        Err(error) => return Err(error),
295    };
296    if read {
297        authorize(&caps, &target, &path, false)?;
298    }
299    if write {
300        authorize(&caps, &target, &path, true)?;
301    }
302    let (directory, name) = parent(target.as_path())?;
303    match directory.symlink_metadata(&name) {
304        Ok(_) if create_new => {
305            return Err(io::Error::new(
306                io::ErrorKind::AlreadyExists,
307                "File already exists",
308            ));
309        }
310        Ok(metadata) if !metadata.is_file() => {
311            return Err(io::Error::new(
312                non_regular,
313                "not yet implemented: opening non-regular files",
314            ));
315        }
316        Ok(_) => (),
317        Err(error) if error.kind() == io::ErrorKind::NotFound => (),
318        Err(error) => return Err(error),
319    }
320    let mut flags = OpenOptions::new();
321    flags
322        .read(read)
323        .write(write)
324        .append(append)
325        .create(create)
326        .create_new(create_new)
327        .follow(FollowSymlinks::No)
328        .nonblock(true);
329    #[cfg(unix)]
330    {
331        use cap_std::fs::OpenOptionsExt;
332        if let Some(mode) = mode {
333            flags.mode(mode);
334        }
335    }
336    let file = directory.open_with(name, &flags)?;
337    if !file.metadata()?.is_file() {
338        return Err(io::Error::new(
339            non_regular,
340            "not yet implemented: opening non-regular files",
341        ));
342    }
343    let opened = Opened {
344        file,
345        target,
346        label: path,
347        caps: caps.clone(),
348        read,
349        write,
350    };
351    // Truncation must not happen until the opened object is verified and authorized.
352    if truncate {
353        opened.check(true)?;
354        opened.file.set_len(0)?;
355    }
356    Ok(opened)
357}
358
359#[cfg_attr(
360    not(any(target_os = "linux", target_os = "macos", windows)),
361    allow(dead_code)
362)]
363pub fn process_file(
364    caps: Arc<Capabilities>,
365    path: String,
366    options: Options,
367) -> io::Result<std::fs::File> {
368    Ok(open_file(caps, path, options, io::ErrorKind::Unsupported)?
369        .file
370        .into_std())
371}
372
373pub fn response_file(
374    caps: Arc<Capabilities>,
375    path: String,
376) -> io::Result<(std::fs::File, avril_http::file::CheckFile)> {
377    let opened = open_file(
378        caps,
379        path,
380        (true, false, false, false, false, false, None),
381        io::ErrorKind::NotFound,
382    )?;
383    opened.check(false)?;
384    let Opened { file, target, label, caps, .. } = opened;
385    let check = Arc::new(move || authorize(&caps, &target, &label, false));
386    Ok((file.into_std(), check))
387}
388
389pub struct Files {
390    pool: BlockingPool,
391    resources: Mutex<HashMap<u64, Arc<Resource>>>,
392    next: AtomicU64,
393    caps: Arc<Capabilities>,
394    interrupted: Arc<AtomicBool>,
395}
396
397impl Drop for Files {
398    fn drop(&mut self) {
399        for resource in self
400            .resources
401            .get_mut()
402            .unwrap()
403            .drain()
404            .map(|(_, resource)| resource)
405        {
406            resource.close();
407        }
408    }
409}
410
411impl Files {
412    fn io_ops<A, R, F>(
413        &self,
414        name: &'static str,
415        sync_name: &'static str,
416        function: impl Fn(A) -> F + Clone + 'static,
417    ) -> Vec<Op>
418    where
419        A: serde::de::DeserializeOwned,
420        R: serde::Serialize + 'static,
421        F: std::future::Future<Output = Result<R, Infallible>> + 'static,
422    {
423        let blocking = function.clone();
424        let interrupted = self.interrupted.clone();
425        vec![
426            Op::asynchronous(name, function),
427            Op::with_scope(sync_name, move |scope, args| {
428                avril_host::wait(blocking(args), || {
429                    avril_host::interrupted(scope, &interrupted)
430                })
431                .transpose()
432            }),
433        ]
434    }
435
436    #[cfg_attr(
437        not(any(target_os = "linux", target_os = "macos", windows)),
438        allow(dead_code)
439    )]
440    pub fn duplicate_for_process(
441        &self,
442        id: u64,
443        read: bool,
444        write: bool,
445    ) -> io::Result<std::fs::File> {
446        let resource = self
447            .resources
448            .lock()
449            .unwrap()
450            .get(&id)
451            .cloned()
452            .ok_or_else(bad_resource)?;
453        if resource.closed.load(Ordering::SeqCst) {
454            return Err(bad_resource());
455        }
456        let queue = resource.queue.lock().unwrap();
457        if queue.running || !queue.operations.is_empty() {
458            return Err(io::Error::new(
459                io::ErrorKind::WouldBlock,
460                "File handle is busy",
461            ));
462        }
463        let file = resource
464            .file
465            .try_lock()
466            .map_err(|_| io::Error::new(io::ErrorKind::WouldBlock, "File handle is busy"))?;
467        let opened = file.as_ref().ok_or_else(bad_resource)?;
468        if read {
469            opened.check(false)?;
470        }
471        if write {
472            opened.check(true)?;
473        }
474        Ok(opened.file.try_clone()?.into_std())
475    }
476
477    pub fn response_handle(
478        &self,
479        id: u64,
480        consume: bool,
481    ) -> io::Result<(String, std::fs::File, avril_http::file::CheckFile)> {
482        let mut resources = self.resources.lock().unwrap();
483        let resource = resources.get(&id).cloned().ok_or_else(bad_resource)?;
484        let queue = resource.queue.lock().unwrap();
485        if resource.closed.load(Ordering::Acquire) {
486            return Err(bad_resource());
487        }
488        if queue.running || !queue.operations.is_empty() {
489            return Err(io::Error::new(
490                io::ErrorKind::WouldBlock,
491                "File handle is busy",
492            ));
493        }
494        let mut file = resource
495            .file
496            .try_lock()
497            .map_err(|_| io::Error::new(io::ErrorKind::WouldBlock, "File handle is busy"))?;
498        let opened = file.as_ref().ok_or_else(bad_resource)?;
499        opened.check(false)?;
500        let name = opened.label.clone();
501        let target = opened.target.clone();
502        let caps = opened.caps.clone();
503        let label = name.clone();
504        let check = Arc::new(move || authorize(&caps, &target, &label, false));
505        let file = if consume {
506            resource.closed.store(true, Ordering::Release);
507            let opened = file.take().unwrap();
508            resources.remove(&id);
509            opened.file.into_std()
510        } else {
511            #[cfg(unix)]
512            let duplicate = opened.file.try_clone()?;
513            #[cfg(windows)]
514            let duplicate = {
515                use cap_fs_ext::Reopen;
516                // Windows positional reads still change a duplicated handle's shared cursor.
517                opened.file.reopen(OpenOptions::new().read(true))?
518            };
519            #[cfg(not(any(unix, windows)))]
520            return Err(io::Error::new(
521                io::ErrorKind::Unsupported,
522                "Retaining file response handles is not supported on this platform",
523            ));
524            #[cfg(any(unix, windows))]
525            duplicate.into_std()
526        };
527        Ok((name, file, check))
528    }
529
530    fn submit<T: Send + 'static>(
531        &self,
532        id: u64,
533        operation: impl FnOnce(&mut Opened) -> io::Result<T> + Send + 'static,
534    ) -> Reply<T> {
535        let resource = self.resources.lock().unwrap().get(&id).cloned();
536        match resource {
537            Some(resource) => resource.submit(operation),
538            None => failed(bad_resource()),
539        }
540    }
541
542    fn submit_observed<T: Send + 'static>(
543        &self,
544        id: u64,
545        operation: impl FnOnce(&mut Opened, &AtomicBool) -> io::Result<T> + Send + 'static,
546    ) -> Reply<T> {
547        let resource = self.resources.lock().unwrap().get(&id).cloned();
548        match resource {
549            Some(resource) => resource.submit_observed(operation),
550            None => failed(bad_resource()),
551        }
552    }
553
554    fn open(self: &Arc<Self>, path: String, options: Options) -> Reply<u64> {
555        let state = self.clone();
556        self.pool.run(move || {
557            let opened = open_file(
558                state.caps.clone(),
559                path,
560                options,
561                io::ErrorKind::Unsupported,
562            )?;
563            let id = state
564                .next
565                .fetch_update(Ordering::AcqRel, Ordering::Acquire, |id| {
566                    (id <= MAX_ID).then_some(id + 1)
567                })
568                .map_err(|_| io::Error::other("File handle identifiers exhausted"))?;
569            state.resources.lock().unwrap().insert(
570                id,
571                Arc::new(Resource {
572                    closed: Arc::new(AtomicBool::new(false)),
573                    queue: Mutex::new(Queue {
574                        operations: VecDeque::new(),
575                        running: false,
576                    }),
577                    file: Mutex::new(Some(opened)),
578                    pool: state.pool.clone(),
579                }),
580            );
581            Ok(id)
582        })
583    }
584}
585
586pub(crate) use avril_caps::fs::parent;
587
588fn info(metadata: Metadata) -> Info {
589    fn time(time: io::Result<cap_std::time::SystemTime>) -> Option<f64> {
590        // Integer milliseconds stay exact through Date's float conversion;
591        // float seconds would smear sub-millisecond remainders into the
592        // millisecond field. Float arithmetic also cannot overflow: times
593        // past the Date range read back as Invalid Date, as before.
594        fn millis(duration: std::time::Duration) -> f64 {
595            duration.as_secs() as f64 * 1000.0 + f64::from(duration.subsec_millis())
596        }
597        let time = time.ok()?.into_std();
598        Some(match time.duration_since(std::time::UNIX_EPOCH) {
599            Ok(duration) => millis(duration),
600            Err(error) => -millis(error.duration()),
601        })
602    }
603    #[cfg(unix)]
604    let (mode, uid, gid) = {
605        use cap_std::fs::MetadataExt;
606        (
607            Some(metadata.mode()),
608            Some(metadata.uid()),
609            Some(metadata.gid()),
610        )
611    };
612    #[cfg(not(unix))]
613    let (mode, uid, gid) = (None, None, None);
614    (
615        metadata.is_file(),
616        metadata.is_dir(),
617        metadata.is_symlink(),
618        metadata.len(),
619        time(metadata.modified()),
620        time(metadata.accessed()),
621        time(metadata.created()),
622        mode,
623        uid,
624        gid,
625    )
626}
627
628pub fn init(
629    runtime: Handle,
630    caps: Arc<Capabilities>,
631    interrupted: Arc<AtomicBool>,
632) -> (Extension, Arc<Files>) {
633    let state = Arc::new(Files {
634        pool: BlockingPool { runtime },
635        resources: Mutex::new(HashMap::new()),
636        next: AtomicU64::new(1),
637        caps,
638        interrupted,
639    });
640    let ops = state.clone();
641    let shared = state.clone();
642    let open = state.clone();
643    let read = state.clone();
644    let write = state.clone();
645    let writev = state.clone();
646    let seek = state.clone();
647    let truncate = state.clone();
648    let sync = state.clone();
649    let stat = state.clone();
650    let path_stat = state.clone();
651    let link_ops = links::ops(state.clone());
652    let path_ops = paths::ops(state.clone());
653    let posix_ops = posix::ops(state.clone());
654    let workflow_ops = workflows::ops(state.clone());
655    let watch_ops = watch::ops(state.clone());
656    let mut extension = Extension {
657        name: "ext:file",
658        dependencies: &["ext:url", "ext:web", "ext:webstreams"],
659        ops: vec![
660            vec![Op::new("op_file_url_path", |value: String| {
661                let parsed = (|| {
662                    let url = url::Url::parse(&value).map_err(io::Error::other)?;
663                    if url.scheme() != "file" || url.query().is_some() || url.fragment().is_some() {
664                        return Err(io::Error::new(
665                            io::ErrorKind::InvalidInput,
666                            "Expected a file URL without query or fragment",
667                        ));
668                    }
669                    url.to_file_path()
670                        .map_err(|_| {
671                            io::Error::new(io::ErrorKind::InvalidInput, "Invalid file URL")
672                        })?
673                        .into_os_string()
674                        .into_string()
675                        .map_err(|_| {
676                            io::Error::new(io::ErrorKind::InvalidInput, "Non-UTF-8 file URL")
677                        })
678                })();
679                Ok::<_, Infallible>(response(parsed))
680            })],
681            ops.io_ops(
682                "op_file_open",
683                "op_file_open_sync",
684                move |(path, options): (String, Options)| {
685                    let pending = open.open(path, options);
686                    async move { Ok::<_, Infallible>(receive(pending).await) }
687                },
688            ),
689            ops.io_ops(
690                "op_file_read",
691                "op_file_read_sync",
692                move |(id, limit): (u64, usize)| {
693                    let pending = read.submit(id, move |opened| {
694                        opened.check(false)?;
695                        let mut bytes = vec![0; limit.min(CHUNK)];
696                        if limit == 0 {
697                            return Ok(Some(ToJsBuffer::from(bytes)));
698                        }
699                        let count = loop {
700                            match opened.file.read(&mut bytes) {
701                                Err(error) if error.kind() == io::ErrorKind::Interrupted => {
702                                    continue;
703                                }
704                                result => break result?,
705                            }
706                        };
707                        bytes.truncate(count);
708                        Ok(if count == 0 {
709                            None
710                        } else {
711                            Some(ToJsBuffer::from(bytes))
712                        })
713                    });
714                    async move { Ok::<_, Infallible>(receive(pending).await) }
715                },
716            ),
717            ops.io_ops(
718                "op_file_write",
719                "op_file_write_sync",
720                move |(id, bytes): (u64, JsBuffer)| {
721                    let bytes = bytes[..bytes.len().min(CHUNK)].to_vec();
722                    let pending = write.submit(id, move |opened| {
723                        opened.check(true)?;
724                        loop {
725                            match opened.file.write(&bytes) {
726                                Err(error) if error.kind() == io::ErrorKind::Interrupted => {
727                                    continue;
728                                }
729                                result => return result,
730                            }
731                        }
732                    });
733                    async move { Ok::<_, Infallible>(receive(pending).await) }
734                },
735            ),
736            ops.io_ops(
737                "op_file_writev",
738                "op_file_writev_sync",
739                move |(id, buffers): (u64, Vec<JsBuffer>)| {
740                    let pending = writev.submit_observed(id, move |opened, closed| {
741                        opened.check(true)?;
742                        let mut total = 0;
743                        let mut elem = 0;
744                        let mut inner = 0;
745                        while elem < buffers.len() {
746                            while elem < buffers.len() && inner >= buffers[elem].len() {
747                                elem += 1;
748                                inner = 0;
749                            }
750                            if elem >= buffers.len() {
751                                break;
752                            }
753                            if closed.load(Ordering::Acquire) {
754                                return Err(bad_resource());
755                            }
756                            opened.check(true)?;
757                            let mut slices = Vec::new();
758                            let mut budget = CHUNK;
759                            let mut scan = elem;
760                            let mut scan_inner = inner;
761                            while scan < buffers.len() && slices.len() < MAX_SLICES && budget > 0 {
762                                let bytes: &[u8] = &buffers[scan];
763                                let available = bytes.len().saturating_sub(scan_inner);
764                                if available == 0 {
765                                    scan += 1;
766                                    scan_inner = 0;
767                                    continue;
768                                }
769                                let take = available.min(budget);
770                                slices
771                                    .push(io::IoSlice::new(&bytes[scan_inner..scan_inner + take]));
772                                budget -= take;
773                                scan_inner += take;
774                                if scan_inner >= bytes.len() {
775                                    scan += 1;
776                                    scan_inner = 0;
777                                }
778                            }
779                            if slices.is_empty() {
780                                break;
781                            }
782                            let count = loop {
783                                match opened.file.write_vectored(&slices) {
784                                    Err(error) if error.kind() == io::ErrorKind::Interrupted => {
785                                        continue;
786                                    }
787                                    result => break result?,
788                                }
789                            };
790                            if count == 0 {
791                                return Err(io::Error::new(
792                                    io::ErrorKind::WriteZero,
793                                    "File write made no progress",
794                                ));
795                            }
796                            total += count;
797                            let mut skip = count;
798                            while skip > 0 {
799                                let remaining = buffers[elem].len() - inner;
800                                if remaining == 0 {
801                                    elem += 1;
802                                    inner = 0;
803                                    continue;
804                                }
805                                if skip < remaining {
806                                    inner += skip;
807                                    break;
808                                }
809                                skip -= remaining;
810                                elem += 1;
811                                inner = 0;
812                            }
813                        }
814                        Ok(total)
815                    });
816                    async move { Ok::<_, Infallible>(receive(pending).await) }
817                },
818            ),
819            ops.io_ops(
820                "op_file_seek",
821                "op_file_seek_sync",
822                move |(id, offset, whence): (u64, i64, u8)| {
823                    let pending = seek.submit(id, move |opened| {
824                        opened.check_metadata()?;
825                        if offset.unsigned_abs() > SAFE_INTEGER {
826                            return Err(io::Error::new(
827                                io::ErrorKind::InvalidInput,
828                                "Unsafe seek offset",
829                            ));
830                        }
831                        let origin = match whence {
832                            0 => 0,
833                            1 => opened.file.stream_position()?,
834                            2 => opened.file.metadata()?.len(),
835                            _ => {
836                                return Err(io::Error::new(
837                                    io::ErrorKind::InvalidInput,
838                                    "Invalid seek mode",
839                                ));
840                            }
841                        };
842                        let position = origin
843                            .checked_add_signed(offset)
844                            .filter(|position| *position <= SAFE_INTEGER)
845                            .ok_or_else(|| {
846                                io::Error::new(io::ErrorKind::InvalidInput, "Unsafe seek position")
847                            })?;
848                        let position = opened.file.seek(SeekFrom::Start(position))?;
849                        Ok(position)
850                    });
851                    async move { Ok::<_, Infallible>(receive(pending).await) }
852                },
853            ),
854            ops.io_ops(
855                "op_file_truncate",
856                "op_file_truncate_sync",
857                move |(id, length): (u64, u64)| {
858                    let pending = truncate.submit(id, move |opened| {
859                        opened.check(true)?;
860                        if length > SAFE_INTEGER {
861                            return Err(io::Error::new(
862                                io::ErrorKind::InvalidInput,
863                                "Unsafe truncate length",
864                            ));
865                        }
866                        opened.file.set_len(length).map(|_| true)
867                    });
868                    async move { Ok::<_, Infallible>(receive(pending).await) }
869                },
870            ),
871            ops.io_ops(
872                "op_file_sync",
873                "op_file_sync_sync",
874                move |(id, data): (u64, bool)| {
875                    let pending = sync.submit(id, move |opened| {
876                        opened.check(true)?;
877                        if data {
878                            opened.file.sync_data()?;
879                        } else {
880                            opened.file.sync_all()?;
881                        }
882                        Ok(true)
883                    });
884                    async move { Ok::<_, Infallible>(receive(pending).await) }
885                },
886            ),
887            ops.io_ops("op_file_stat", "op_file_stat_sync", move |id: u64| {
888                let pending = stat.submit(id, |opened| {
889                    opened.check_metadata()?;
890                    opened.file.metadata().map(info)
891                });
892                async move { Ok::<_, Infallible>(receive(pending).await) }
893            }),
894            ops.io_ops(
895                "op_file_path_stat",
896                "op_file_path_stat_sync",
897                move |(path, follow): (String, bool)| {
898                    let caps = path_stat.caps.clone();
899                    let pending = path_stat.pool.run(move || {
900                        if path.is_empty() {
901                            return Err(io::Error::new(
902                                io::ErrorKind::NotFound,
903                                "Empty filesystem path",
904                            ));
905                        }
906                        let follow = follow || paths::follows_link(&path);
907                        let absolute = filesystem_path(&caps, &path)?;
908                        let target = if follow {
909                            PathTarget::for_creation(&absolute)
910                        } else {
911                            PathTarget::for_entry(&absolute)
912                        };
913                        let target = match target {
914                            Ok(target) => target,
915                            Err(_) if !caps.unrestricted_read() => {
916                                return Err(denied(&path, false));
917                            }
918                            Err(error) => return Err(error),
919                        };
920                        authorize(&caps, &target, &path, false)?;
921                        let (directory, name) = parent(target.as_path())?;
922                        let metadata = directory.symlink_metadata(name)?;
923                        if follow && metadata.is_symlink() {
924                            return Err(io::Error::new(
925                                io::ErrorKind::NotFound,
926                                "Symbolic link changed or has a missing target",
927                            ));
928                        }
929                        Ok(info(metadata))
930                    });
931                    async move { Ok::<_, Infallible>(receive(pending).await) }
932                },
933            ),
934            vec![Op::new("op_file_close", move |(id, quiet): (u64, bool)| {
935                let resource = state.resources.lock().unwrap().remove(&id);
936                let result = match resource {
937                    Some(resource) => {
938                        resource.close();
939                        Ok(true)
940                    }
941                    None if quiet => Ok(true),
942                    None => Err(bad_resource()),
943                };
944                Ok::<_, Infallible>(response(result))
945            })],
946        ]
947        .into_iter()
948        .flatten()
949        .collect(),
950        modules: const {
951            &[
952                (
953                    "ext:file/main.js",
954                    include_str!(concat!(env!("OUT_DIR"), "/main.js")),
955                ),
956                (
957                    "ext:file/errors.js",
958                    include_str!(concat!(env!("OUT_DIR"), "/errors.js")),
959                ),
960                (
961                    "ext:file/sync.js",
962                    include_str!(concat!(env!("OUT_DIR"), "/sync.js")),
963                ),
964                PUBLIC_MODULES[0].module,
965                PUBLIC_MODULES[1].module,
966            ]
967        },
968        entry_point: None,
969    };
970    extension.ops.extend(link_ops);
971    extension.ops.extend(path_ops);
972    extension.ops.extend(posix_ops);
973    extension.ops.extend(workflow_ops);
974    extension.ops.extend(watch_ops);
975    (extension, shared)
976}
977
978#[cfg(test)]
979mod tests {
980    use super::*;
981
982    #[tokio::test]
983    async fn response_handles_reject_busy_files_without_consuming_them() {
984        let root = tempfile::tempdir().unwrap();
985        std::fs::write(root.path().join("file.txt"), b"content").unwrap();
986        let caps = Capabilities::new(avril_caps::Initial::new(false), root.path().to_owned());
987        let (_extension, files) = init(Handle::current(), caps, Arc::new(AtomicBool::new(false)));
988        let id = files
989            .open(
990                "file.txt".into(),
991                (true, false, false, false, false, false, None),
992            )
993            .await
994            .unwrap()
995            .unwrap();
996        let (started, ready) = oneshot::channel();
997        let (release, wait) = std::sync::mpsc::channel();
998        let operation = files.submit(id, move |_| {
999            started.send(()).unwrap();
1000            wait.recv().unwrap();
1001            Ok(())
1002        });
1003        ready.await.unwrap();
1004        for consume in [false, true] {
1005            assert_eq!(
1006                files.response_handle(id, consume).err().unwrap().kind(),
1007                io::ErrorKind::WouldBlock,
1008            );
1009        }
1010        release.send(()).unwrap();
1011        operation.await.unwrap().unwrap();
1012        let (name, file, check) = files.response_handle(id, true).unwrap();
1013        assert_eq!(name, "file.txt");
1014        check().unwrap();
1015        assert_eq!(file.metadata().unwrap().len(), 7);
1016        assert_eq!(
1017            files.response_handle(id, false).err().unwrap().kind(),
1018            io::ErrorKind::NotConnected,
1019        );
1020    }
1021}