char/avril

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

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

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
29.7 KiB917 linesraw
1use super::paths::{resolve, tree};
2use super::*;
3
4use std::collections::HashSet;
5use tokio::sync::Notify;
6
7const MAX_ROOTS: usize = 128;
8const MAX_QUEUED_EVENTS: usize = 256;
9const MAX_QUEUED_PATHS: usize = 4096;
10
11const CREATE: u8 = 0;
12const MODIFY: u8 = 1;
13const REMOVE: u8 = 2;
14const RENAME: u8 = 3;
15const RESCAN: u8 = 4;
16
17type WatchEvent = (u8, Vec<String>);
18
19#[derive(Clone)]
20struct Root {
21    target: PathTarget,
22    path: PathBuf,
23    directory: bool,
24    recursive: bool,
25    // A file root is watched through its parent directory, so replacing the
26    // file (the usual atomic save) or recreating it stays observable at the
27    // same path instead of following the old inode.
28    watch_path: PathBuf,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32struct QueuedEvent {
33    kind: u8,
34    paths: Vec<PathBuf>,
35    ends: Vec<usize>,
36}
37
38#[derive(Default)]
39struct WatchState {
40    queue: VecDeque<QueuedEvent>,
41    closed: bool,
42    terminal: Option<(io::ErrorKind, String)>,
43    lossy: bool,
44    live: Vec<bool>,
45    // Root endings discarded with an overflowed queue, applied when the
46    // replacement rescan is delivered instead.
47    pending_ends: Vec<usize>,
48    epoch: u32,
49}
50
51struct Shared {
52    caps: Arc<Capabilities>,
53    roots: Vec<Root>,
54    state: Mutex<WatchState>,
55    notify: Notify,
56}
57
58struct WatchResource {
59    shared: Arc<Shared>,
60    #[cfg_attr(
61        not(any(
62            target_os = "linux",
63            target_os = "android",
64            target_os = "macos",
65            target_os = "windows"
66        )),
67        allow(dead_code)
68    )]
69    native: Mutex<Option<notify::RecommendedWatcher>>,
70}
71
72impl WatchResource {
73    fn close(&self) {
74        {
75            let mut state = self.shared.state.lock().unwrap();
76            state.closed = true;
77            state.queue.clear();
78            state.lossy = false;
79            state.epoch = state.epoch.wrapping_add(1);
80        }
81        self.shared.notify.notify_waiters();
82        let native = self.native.lock().unwrap().take();
83        drop(native);
84        self.shared.notify.notify_waiters();
85    }
86}
87
88struct Watchers {
89    caps: Arc<Capabilities>,
90    resources: Mutex<HashMap<u64, Arc<WatchResource>>>,
91    next: AtomicU64,
92}
93
94impl Drop for Watchers {
95    fn drop(&mut self) {
96        for resource in self
97            .resources
98            .get_mut()
99            .unwrap()
100            .drain()
101            .map(|(_, resource)| resource)
102        {
103            resource.close();
104        }
105    }
106}
107
108impl Watchers {
109    fn resource(&self, id: u64) -> io::Result<Arc<WatchResource>> {
110        self.resources
111            .lock()
112            .unwrap()
113            .get(&id)
114            .cloned()
115            .ok_or_else(bad_resource)
116    }
117
118    fn create(&self, labels: Vec<String>, recursive: bool) -> io::Result<u64> {
119        if labels.is_empty() || labels.len() > MAX_ROOTS {
120            return Err(io::Error::new(
121                io::ErrorKind::InvalidInput,
122                "Expected between one and 128 filesystem watch paths",
123            ));
124        }
125        let roots = roots(&self.caps, labels, recursive)?;
126        let live = vec![true; roots.len()];
127        let shared = Arc::new(Shared {
128            caps: self.caps.clone(),
129            roots,
130            state: Mutex::new(WatchState { live, ..WatchState::default() }),
131            notify: Notify::new(),
132        });
133        let native = register(&shared)?;
134        check_roots_unchanged(&shared)?;
135        let id = self
136            .next
137            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |id| {
138                (id <= super::MAX_ID).then_some(id + 1)
139            })
140            .map_err(|_| io::Error::other("Filesystem watcher identifiers exhausted"))?;
141        self.resources.lock().unwrap().insert(
142            id,
143            Arc::new(WatchResource {
144                shared,
145                native: Mutex::new(Some(native)),
146            }),
147        );
148        Ok(id)
149    }
150
151    fn close(&self, id: u64, quiet: bool) -> io::Result<bool> {
152        let resource = self.resources.lock().unwrap().remove(&id);
153        match resource {
154            Some(resource) => {
155                resource.close();
156                Ok(true)
157            }
158            None if quiet => Ok(true),
159            None => Err(bad_resource()),
160        }
161    }
162}
163
164fn roots(caps: &Capabilities, labels: Vec<String>, recursive: bool) -> io::Result<Vec<Root>> {
165    let mut roots = Vec::new();
166    let mut seen = HashSet::new();
167    for label in labels {
168        let target = resolve(caps, &label, false, false)?;
169        let path = target.as_path().to_owned();
170        if !seen.insert(path.clone()) {
171            continue;
172        }
173        let (directory, name) = parent(&path)?;
174        let metadata = directory.symlink_metadata(&name)?;
175        if metadata.is_symlink() {
176            return Err(io::Error::new(
177                io::ErrorKind::WouldBlock,
178                "Filesystem watch root changed while it was registered",
179            ));
180        }
181        let (directory, watch_path) = if metadata.is_dir() {
182            (true, path.clone())
183        } else if metadata.is_file() {
184            let parent = path.parent().ok_or_else(|| {
185                io::Error::new(io::ErrorKind::InvalidInput, "Watched file has no parent")
186            })?;
187            (false, parent.to_owned())
188        } else {
189            return Err(io::Error::new(
190                io::ErrorKind::Unsupported,
191                "Only regular files and directories can be watched",
192            ));
193        };
194        if directory && recursive {
195            tree(caps, &target, &label, false)?;
196        }
197        roots.push(Root {
198            target,
199            path,
200            directory,
201            recursive: directory && recursive,
202            watch_path,
203        });
204    }
205    Ok(roots)
206}
207
208fn check_roots_unchanged(shared: &Shared) -> io::Result<()> {
209    for root in &shared.roots {
210        let current = PathTarget::for_creation(&root.path).map_err(|_| {
211            io::Error::new(
212                io::ErrorKind::WouldBlock,
213                "Filesystem watch root changed while it was registered",
214            )
215        })?;
216        if current.as_path() != root.path {
217            return Err(io::Error::new(
218                io::ErrorKind::WouldBlock,
219                "Filesystem watch root changed while it was registered",
220            ));
221        }
222        let (directory, name) = parent(&root.path)?;
223        let metadata = directory.symlink_metadata(&name)?;
224        if metadata.is_symlink() || metadata.is_dir() != root.directory {
225            return Err(io::Error::new(
226                io::ErrorKind::WouldBlock,
227                "Filesystem watch root changed while it was registered",
228            ));
229        }
230    }
231    Ok(())
232}
233
234#[cfg(any(
235    target_os = "linux",
236    target_os = "android",
237    target_os = "macos",
238    target_os = "windows"
239))]
240fn register(shared: &Arc<Shared>) -> io::Result<notify::RecommendedWatcher> {
241    use notify::Watcher;
242
243    let weak = Arc::downgrade(shared);
244    let handler = move |result: notify::Result<notify::Event>| {
245        if let Some(shared) = weak.upgrade() {
246            shared.handle(result);
247        }
248    };
249    let config = notify::Config::default().with_follow_symlinks(false);
250    let mut watcher =
251        <notify::RecommendedWatcher as Watcher>::new(handler, config).map_err(watch_error)?;
252    // Roots can share a registration path (a watched file and its parent
253    // directory); the recursive registration must win when both are present.
254    let mut registrations: HashMap<PathBuf, bool> = HashMap::new();
255    for root in &shared.roots {
256        let recursive = root.directory && root.recursive;
257        registrations
258            .entry(root.watch_path.clone())
259            .and_modify(|existing| *existing |= recursive)
260            .or_insert(recursive);
261    }
262    for (path, recursive) in registrations {
263        let mode = if recursive {
264            notify::RecursiveMode::Recursive
265        } else {
266            notify::RecursiveMode::NonRecursive
267        };
268        watcher.watch(&path, mode).map_err(watch_error)?;
269    }
270    Ok(watcher)
271}
272
273#[cfg(not(any(
274    target_os = "linux",
275    target_os = "android",
276    target_os = "macos",
277    target_os = "windows"
278)))]
279fn register(_shared: &Arc<Shared>) -> io::Result<notify::RecommendedWatcher> {
280    Err(io::Error::new(
281        io::ErrorKind::Unsupported,
282        "Filesystem watching is not supported on this platform",
283    ))
284}
285
286fn watch_error(error: notify::Error) -> io::Error {
287    let message = error.to_string();
288    match error.kind {
289        notify::ErrorKind::PathNotFound | notify::ErrorKind::WatchNotFound => {
290            io::Error::new(io::ErrorKind::NotFound, message)
291        }
292        notify::ErrorKind::Io(inner) => io::Error::new(inner.kind(), message),
293        notify::ErrorKind::InvalidConfig(_) => io::Error::new(io::ErrorKind::InvalidInput, message),
294        notify::ErrorKind::MaxFilesWatch => io::Error::new(io::ErrorKind::WouldBlock, message),
295        notify::ErrorKind::Generic(_) => io::Error::other(message),
296    }
297}
298
299impl Shared {
300    fn handle(self: &Arc<Self>, result: notify::Result<notify::Event>) {
301        match result {
302            Ok(event) => {
303                if event.need_rescan() {
304                    self.enqueue(QueuedEvent {
305                        kind: RESCAN,
306                        paths: self.event_roots(&event.paths),
307                        ends: Vec::new(),
308                    });
309                    return;
310                }
311                let Some(kind) = map_kind(&event.kind, event.paths.len()) else {
312                    return;
313                };
314                if event.paths.is_empty() {
315                    return;
316                }
317                let mut paths = Vec::new();
318                let mut unknown = false;
319                for path in event.paths {
320                    if self.root_index(&path).is_some() || kind == RENAME {
321                        paths.push(path);
322                    } else if self.in_scope(&path) {
323                        // The backend also sees siblings of a watched file and,
324                        // on some platforms, descendants of a non-recursive
325                        // directory. Those are in scope but not reportable.
326                        continue;
327                    } else {
328                        unknown = true;
329                    }
330                }
331                if unknown && kind != RENAME {
332                    self.enqueue(QueuedEvent {
333                        kind: RESCAN,
334                        paths: self.roots.iter().map(|root| root.path.clone()).collect(),
335                        ends: Vec::new(),
336                    });
337                    return;
338                }
339                if paths.is_empty() {
340                    return;
341                }
342                let ends = self.event_ends(kind, &paths);
343                self.enqueue(QueuedEvent { kind, paths, ends });
344            }
345            Err(error) => {
346                let error = watch_error(error);
347                let mut state = self.state.lock().unwrap();
348                if !state.closed && state.terminal.is_none() {
349                    state.terminal = Some((error.kind(), error.to_string()));
350                    drop(state);
351                    self.notify.notify_waiters();
352                }
353            }
354        }
355    }
356
357    fn event_roots(&self, paths: &[PathBuf]) -> Vec<PathBuf> {
358        let mut roots = Vec::new();
359        for path in paths {
360            if let Some(index) = self.root_index(path) {
361                let root = &self.roots[index];
362                if !roots.contains(&root.path) {
363                    roots.push(root.path.clone());
364                }
365            }
366        }
367        if roots.is_empty() {
368            roots = self.roots.iter().map(|root| root.path.clone()).collect();
369        }
370        roots
371    }
372
373    fn event_ends(&self, kind: u8, paths: &[PathBuf]) -> Vec<usize> {
374        let mut ends = Vec::new();
375        if kind != REMOVE && kind != RENAME {
376            return ends;
377        }
378        for (index, root) in self.roots.iter().enumerate() {
379            if !root.directory {
380                // A watched file is anchored at its path, not its inode, and
381                // may be removed and recreated without ending the watch.
382                continue;
383            }
384            let ends_at_root = if kind == REMOVE {
385                paths.iter().any(|path| path == &root.path)
386            } else {
387                paths.first() == Some(&root.path)
388            };
389            if ends_at_root {
390                ends.push(index);
391            }
392        }
393        ends
394    }
395
396    fn root_index(&self, path: &Path) -> Option<usize> {
397        self.roots
398            .iter()
399            .enumerate()
400            .filter(|(_, root)| {
401                if !root.directory {
402                    return path == root.path;
403                }
404                if path == root.path {
405                    return true;
406                }
407                let Ok(relative) = path.strip_prefix(&root.path) else {
408                    return false;
409                };
410                root.recursive || relative.components().count() == 1
411            })
412            .max_by_key(|(_, root)| root.path.components().count())
413            .map(|(index, _)| index)
414    }
415
416    fn in_scope(&self, path: &Path) -> bool {
417        self.roots
418            .iter()
419            .any(|root| path.starts_with(&root.watch_path))
420    }
421
422    fn enqueue(&self, event: QueuedEvent) {
423        let mut state = self.state.lock().unwrap();
424        if state.closed {
425            return;
426        }
427        if state.lossy {
428            state.pending_ends.extend(event.ends.iter().copied());
429            return;
430        }
431        if event.kind == RESCAN {
432            if let Some(existing) = state.queue.iter_mut().find(|queued| queued.kind == RESCAN) {
433                for path in event.paths {
434                    if !existing.paths.contains(&path) {
435                        existing.paths.push(path);
436                    }
437                }
438                drop(state);
439                self.notify.notify_waiters();
440                return;
441            }
442        } else if event.kind != RENAME
443            && state.queue.back().is_some_and(|last| {
444                last.kind == event.kind && last.ends.is_empty() && event.ends.is_empty()
445            })
446        {
447            let queued_paths = state
448                .queue
449                .iter()
450                .map(|queued| queued.paths.len())
451                .sum::<usize>();
452            let new_paths = {
453                let last = state.queue.back().unwrap();
454                event
455                    .paths
456                    .iter()
457                    .filter(|path| !last.paths.contains(path))
458                    .count()
459            };
460            if queued_paths + new_paths <= MAX_QUEUED_PATHS {
461                let last = state.queue.back_mut().unwrap();
462                for path in event.paths {
463                    if !last.paths.contains(&path) {
464                        last.paths.push(path);
465                    }
466                }
467                drop(state);
468                self.notify.notify_waiters();
469                return;
470            }
471        }
472        let queued_paths = state
473            .queue
474            .iter()
475            .map(|queued| queued.paths.len())
476            .sum::<usize>();
477        if state.queue.len() >= MAX_QUEUED_EVENTS
478            || queued_paths + event.paths.len() > MAX_QUEUED_PATHS
479        {
480            let paths = self
481                .roots
482                .iter()
483                .enumerate()
484                .filter(|(index, _)| state.live.get(*index).copied().unwrap_or(false))
485                .map(|(_, root)| root.path.clone())
486                .collect::<Vec<_>>();
487            let mut discarded: Vec<usize> = state
488                .queue
489                .drain(..)
490                .flat_map(|queued| queued.ends)
491                .collect();
492            discarded.extend(event.ends.iter().copied());
493            state.pending_ends.extend(discarded);
494            state.queue.push_back(QueuedEvent {
495                kind: RESCAN,
496                paths,
497                ends: Vec::new(),
498            });
499            state.lossy = true;
500        } else {
501            state.queue.push_back(event);
502        }
503        drop(state);
504        self.notify.notify_waiters();
505    }
506
507    async fn next(&self) -> io::Result<Option<WatchEvent>> {
508        loop {
509            let notified = self.notify.notified();
510            tokio::pin!(notified);
511            notified.as_mut().enable();
512            let candidate = {
513                let mut state = self.state.lock().unwrap();
514                if state.closed {
515                    return Ok(None);
516                }
517                if let Some(mut event) = state.queue.pop_front() {
518                    if event.kind == RESCAN {
519                        state.lossy = false;
520                        event.ends.append(&mut state.pending_ends);
521                    }
522                    Some(event)
523                } else if let Some((kind, message)) = state.terminal.take() {
524                    state.closed = true;
525                    return Err(io::Error::new(kind, message));
526                } else if state.live.iter().all(|live| !*live) {
527                    state.closed = true;
528                    return Ok(None);
529                } else {
530                    None
531                }
532            };
533            if let Some(candidate) = candidate {
534                let ends = candidate.ends.clone();
535                let event = self.authorize(candidate);
536                {
537                    let mut state = self.state.lock().unwrap();
538                    for index in ends {
539                        if let Some(live) = state.live.get_mut(index) {
540                            *live = false;
541                        }
542                    }
543                }
544                if let Some(event) = event {
545                    return Ok(Some(event));
546                }
547                if !self.any_root_authorized() {
548                    let mut state = self.state.lock().unwrap();
549                    state.closed = true;
550                    return Err(denied(
551                        &self
552                            .roots
553                            .first()
554                            .map(|root| root.path.display().to_string())
555                            .unwrap_or_default(),
556                        false,
557                    ));
558                }
559                continue;
560            }
561            notified.await;
562        }
563    }
564
565    fn authorize(&self, event: QueuedEvent) -> Option<WatchEvent> {
566        if event.kind == RESCAN {
567            let mut paths = Vec::new();
568            for path in event.paths {
569                let Some(index) = self.root_index(&path) else {
570                    continue;
571                };
572                if !self.state.lock().unwrap().live[index] {
573                    continue;
574                }
575                let root = &self.roots[index];
576                if self.caps.check_read(&root.target).is_err() {
577                    continue;
578                }
579                if let Ok(path) = path.into_os_string().into_string()
580                    && !paths.contains(&path)
581                {
582                    paths.push(path);
583                }
584            }
585            return (!paths.is_empty()).then_some((RESCAN, paths));
586        }
587
588        let mut paths = Vec::new();
589        for path in &event.paths {
590            if !self.path_authorized(path) {
591                continue;
592            }
593            match path.clone().into_os_string().into_string() {
594                Ok(path) => {
595                    if !paths.contains(&path) {
596                        paths.push(path);
597                    }
598                }
599                Err(_) => return self.rescan_for(event.paths.first()),
600            }
601        }
602        if paths.is_empty() {
603            return None;
604        }
605        let kind = if event.kind == RENAME && paths.len() == 1 {
606            let retained = &paths[0];
607            if event.paths.first().and_then(|from| from.to_str()) == Some(retained.as_str()) {
608                REMOVE
609            } else {
610                CREATE
611            }
612        } else {
613            event.kind
614        };
615        Some((kind, paths))
616    }
617
618    fn rescan_for(&self, _path: Option<&PathBuf>) -> Option<WatchEvent> {
619        let mut paths = Vec::new();
620        for root in &self.roots {
621            if self.caps.check_read(&root.target).is_err() {
622                continue;
623            }
624            if let Ok(path) = root.path.clone().into_os_string().into_string() {
625                paths.push(path);
626            }
627        }
628        (!paths.is_empty()).then_some((RESCAN, paths))
629    }
630
631    fn path_authorized(&self, path: &Path) -> bool {
632        let Some(index) = self.root_index(path) else {
633            return false;
634        };
635        let root = &self.roots[index];
636        if !self.state.lock().unwrap().live[index] {
637            return false;
638        }
639        let mut target = root.target.clone();
640        if path != root.path {
641            let Ok(relative) = path.strip_prefix(&root.path) else {
642                return false;
643            };
644            for component in relative.components() {
645                let Component::Normal(name) = component else {
646                    return false;
647                };
648                let Ok(child) = target.child(name) else {
649                    return false;
650                };
651                target = child;
652            }
653        }
654        self.caps.check_read(&target).is_ok()
655    }
656
657    fn any_root_authorized(&self) -> bool {
658        let state = self.state.lock().unwrap();
659        self.roots.iter().enumerate().any(|(index, root)| {
660            state.live.get(index).copied().unwrap_or(false)
661                && self.caps.check_read(&root.target).is_ok()
662        })
663    }
664
665    async fn wait_epoch(&self, epoch: u32) -> bool {
666        loop {
667            let notified = self.notify.notified();
668            tokio::pin!(notified);
669            notified.as_mut().enable();
670            {
671                let state = self.state.lock().unwrap();
672                if state.closed || state.epoch != epoch {
673                    return false;
674                }
675            }
676            notified.await;
677        }
678    }
679
680    fn bump_epoch(&self) -> u32 {
681        let epoch = {
682            let mut state = self.state.lock().unwrap();
683            state.epoch = state.epoch.wrapping_add(1);
684            state.epoch
685        };
686        self.notify.notify_waiters();
687        epoch
688    }
689}
690
691fn map_kind(kind: &notify::EventKind, path_count: usize) -> Option<u8> {
692    use notify::event::{EventKind, ModifyKind, RenameMode};
693
694    match kind {
695        EventKind::Access(_) => None,
696        EventKind::Create(_) => Some(CREATE),
697        EventKind::Modify(ModifyKind::Name(RenameMode::Both)) if path_count == 2 => Some(RENAME),
698        EventKind::Modify(ModifyKind::Name(RenameMode::From)) => Some(REMOVE),
699        EventKind::Modify(ModifyKind::Name(RenameMode::To)) => Some(CREATE),
700        EventKind::Modify(_) => Some(MODIFY),
701        EventKind::Remove(_) => Some(REMOVE),
702        EventKind::Any | EventKind::Other => Some(MODIFY),
703    }
704}
705
706pub(super) fn ops(files: Arc<Files>) -> Vec<Op> {
707    let watchers = Arc::new(Watchers {
708        caps: files.caps.clone(),
709        resources: Mutex::new(HashMap::new()),
710        next: AtomicU64::new(1),
711    });
712    let create = watchers.clone();
713    let next = watchers.clone();
714    let next_unref = watchers.clone();
715    let keepalive = watchers.clone();
716    let reference = watchers.clone();
717    let close = watchers.clone();
718    vec![
719        Op::new(
720            "op_file_watch",
721            move |(paths, recursive): (Vec<String>, bool)| {
722                Ok::<_, Infallible>(response(create.create(paths, recursive)))
723            },
724        ),
725        Op::asynchronous("op_file_watch_next", move |id: u64| {
726            let resource = next.resource(id);
727            async move { Ok::<_, Infallible>(receive_watch(resource).await) }
728        }),
729        Op::asynchronous_unreferenced("op_file_watch_next_unref", move |id: u64| {
730            let resource = next_unref.resource(id);
731            async move { Ok::<_, Infallible>(receive_watch(resource).await) }
732        }),
733        Op::asynchronous("op_file_watch_keepalive", move |(id, epoch): (u64, u32)| {
734            let resource = keepalive.resource(id);
735            async move {
736                let result = match resource {
737                    Ok(resource) => Ok(resource.shared.wait_epoch(epoch).await),
738                    Err(error) => Err(error),
739                };
740                Ok::<_, Infallible>(response(result))
741            }
742        }),
743        Op::new("op_file_watch_ref", move |id: u64| {
744            let result = reference
745                .resource(id)
746                .map(|resource| resource.shared.bump_epoch());
747            Ok::<_, Infallible>(response(result))
748        }),
749        Op::new("op_file_watch_close", move |(id, quiet): (u64, bool)| {
750            Ok::<_, Infallible>(response(close.close(id, quiet)))
751        }),
752    ]
753}
754
755async fn receive_watch(resource: io::Result<Arc<WatchResource>>) -> Response<Option<WatchEvent>> {
756    match resource {
757        Ok(resource) => response(resource.shared.next().await),
758        Err(error) => response(Err(error)),
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn maps_notify_kinds_to_public_kinds() {
768        use notify::event::{CreateKind, EventKind, ModifyKind, RemoveKind, RenameMode};
769
770        assert_eq!(
771            map_kind(&EventKind::Create(CreateKind::File), 1),
772            Some(CREATE)
773        );
774        assert_eq!(
775            map_kind(&EventKind::Remove(RemoveKind::File), 1),
776            Some(REMOVE)
777        );
778        assert_eq!(
779            map_kind(&EventKind::Modify(ModifyKind::Any), 1),
780            Some(MODIFY)
781        );
782        assert_eq!(
783            map_kind(&EventKind::Modify(ModifyKind::Name(RenameMode::Both)), 2),
784            Some(RENAME)
785        );
786        assert_eq!(
787            map_kind(&EventKind::Modify(ModifyKind::Name(RenameMode::Both)), 1),
788            Some(MODIFY)
789        );
790        assert_eq!(
791            map_kind(&EventKind::Modify(ModifyKind::Name(RenameMode::From)), 1),
792            Some(REMOVE)
793        );
794        assert_eq!(
795            map_kind(&EventKind::Modify(ModifyKind::Name(RenameMode::To)), 1),
796            Some(CREATE)
797        );
798    }
799
800    fn shared(root: &Path) -> Arc<Shared> {
801        Arc::new(Shared {
802            caps: Capabilities::new(avril_caps::Initial::new(false), root.to_path_buf()),
803            roots: vec![Root {
804                target: PathTarget::resolve(root).unwrap(),
805                path: root.to_path_buf(),
806                directory: true,
807                recursive: true,
808                watch_path: root.to_path_buf(),
809            }],
810            state: Mutex::new(WatchState {
811                live: vec![true],
812                ..WatchState::default()
813            }),
814            notify: Notify::new(),
815        })
816    }
817
818    #[test]
819    fn coalesces_adjacent_events_without_reordering_kinds() {
820        let directory = tempfile::tempdir().unwrap();
821        let root = directory.path();
822        let shared = shared(root);
823        shared.enqueue(QueuedEvent {
824            kind: MODIFY,
825            paths: vec![root.join("a")],
826            ends: Vec::new(),
827        });
828        shared.enqueue(QueuedEvent {
829            kind: MODIFY,
830            paths: vec![root.join("b"), root.join("a")],
831            ends: Vec::new(),
832        });
833        shared.enqueue(QueuedEvent {
834            kind: CREATE,
835            paths: vec![root.join("c")],
836            ends: Vec::new(),
837        });
838        let state = shared.state.lock().unwrap();
839        assert_eq!(state.queue.len(), 2);
840        assert_eq!(state.queue[0].paths, [root.join("a"), root.join("b")]);
841        assert_eq!(state.queue[1].kind, CREATE);
842    }
843
844    #[test]
845    fn queue_overflow_collapses_to_one_rescan() {
846        let directory = tempfile::tempdir().unwrap();
847        let root = directory.path();
848        let shared = shared(root);
849        for index in 0..=MAX_QUEUED_PATHS {
850            shared.enqueue(QueuedEvent {
851                kind: MODIFY,
852                paths: vec![root.join(index.to_string())],
853                ends: Vec::new(),
854            });
855        }
856        {
857            let state = shared.state.lock().unwrap();
858            assert!(state.lossy);
859            assert_eq!(state.queue.len(), 1);
860            assert_eq!(state.queue[0].kind, RESCAN);
861            assert_eq!(state.queue[0].paths, [root.to_path_buf()]);
862        }
863        shared.enqueue(QueuedEvent {
864            kind: CREATE,
865            paths: vec![root.join("late")],
866            ends: Vec::new(),
867        });
868        assert_eq!(shared.state.lock().unwrap().queue.len(), 1);
869    }
870
871    #[test]
872    fn overflow_preserves_pending_root_ends() {
873        let directory = tempfile::tempdir().unwrap();
874        let root = directory.path();
875        let shared = shared(root);
876        shared.enqueue(QueuedEvent {
877            kind: REMOVE,
878            paths: vec![root.to_path_buf()],
879            ends: vec![0],
880        });
881        for index in 0..=MAX_QUEUED_PATHS {
882            shared.enqueue(QueuedEvent {
883                kind: MODIFY,
884                paths: vec![root.join(index.to_string())],
885                ends: Vec::new(),
886            });
887        }
888        let state = shared.state.lock().unwrap();
889        assert_eq!(state.queue[0].kind, RESCAN);
890        assert_eq!(state.pending_ends, [0]);
891    }
892
893    #[test]
894    fn rename_with_an_unauthorized_destination_becomes_remove() {
895        let directory = tempfile::tempdir().unwrap();
896        let root = directory.path();
897        let shared = shared(root);
898        let secret = root.join("secret");
899        shared
900            .caps
901            .pledge(vec![(
902                avril_caps::Category::Read,
903                None,
904                vec![secret.display().to_string()],
905            )])
906            .unwrap();
907        let event = shared.authorize(QueuedEvent {
908            kind: RENAME,
909            paths: vec![root.join("old"), secret.join("new")],
910            ends: Vec::new(),
911        });
912        assert_eq!(
913            event,
914            Some((REMOVE, vec![root.join("old").display().to_string()]))
915        );
916    }
917}