Repository: tauri-apps/tauri (language: rust)

Issue:
[bug] "forbidden path" when calling `exists` with absolute resource path for non-existing file
### Describe the bug

I'm in the process of making my app compatible with Tauri 2.0 after upgrading to it recently. I'm trying to access files in the resource folder by fist calling `path.exists` like this:

```ts
let filePath = await path.join(await path.resourceDir(), "some_folder", "file.xlsx");
let fileExists = await exists(filePath);
```

This looked pretty much the same on Tauri 1.x and worked, but now on Tauri 2.0 I'm getting an exception:
`forbidden path: D:\path_to_repo\src-tauri\target\debug\some_folder\file.xlsx`

Two weird things about all this:

- This exception only happens if the file does not exist. If it exists, then the code above simply returns `true` as you would expect.
- If I'm calling it with `baseDir` specified (`await exists(filePath, {baseDir: BaseDirectory.Resource})`), it doesn't result in an exception, but instead `exists` always returns `false` (I guess that makes sense now that I think about it).

Although I'm not 100% sure how the new permission system works yet, I think my app should have the correct permissions to interact with the file:

```json
{
  "permissions": [
    "fs:default",
    "fs:allow-desktop-read-recursive",
    "fs:allow-desktop-meta-recursive",
    "fs:allow-resource-read-recursive",
    "fs:allow-resource-meta-recursive"
  ]
}
```

In the end, using a base directory, this code worked:

```ts
let filePath = await path.join("some_folder", "file.xlsx");
let fileExists = await exists(filePath, {baseDir: BaseDirectory.Resource});
```

In order to check whether it is a permission issue or not, I ran the same code for desktop files and experienced no issues doing that. All the results were correct and no exception happened.

### Reproduction

Here are the various tests I did, always for both an existing and a non-existing file. First are the resource accesses with the mentioned exception and the wrong results, followed by the desktop accesses with all correct results.

```ts
let filePath = "";


// Accessing resource
console.log("\n-----------------------");

// Relative path, using join and baseDir
filePath = await path.join("some_folder", "existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Resource})); // true
filePath = await path.join("some_folder", "non_existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Resource})); // false

// Absolute path, using join, but no baseDir
filePath = await path.join(await path.resourceDir(), "some_folder", "existing_file.xlsx");
console.log(filePath, await exists(filePath)); // true
filePath = await path.join(await path.resourceDir(), "some_folder", "non_existing_file.xlsx");
console.log(filePath, await exists(filePath)); // exception, forbidden path

// Absolute path, using join and baseDir
filePath = await path.join(await path.resourceDir(), "some_folder", "existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Resource})); // false, wrong result
filePath = await path.join(await path.resourceDir(), "some_folder", "non_existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Resource})); // false, probably "wrong" result


// Now accessing desktop instead of resource
console.log("\n-----------------------");

// Relative path, using join and baseDir
filePath = await path.join("some_folder", "existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Desktop})); // true
filePath = await path.join("some_folder", "non_existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Desktop})); // false

// Absolute path, using join, but no baseDir
filePath = await path.join(await path.desktopDir(), "some_folder", "existing_file.xlsx");
console.log(filePath, await exists(filePath)); // true
filePath = await path.join(await path.desktopDir(), "some_folder", "non_existing_file.xlsx");
console.log(filePath, await exists(filePath)); // false

// Absolute path, using join and baseDir
filePath = await path.join(await path.desktopDir(), "some_folder", "existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Desktop})); // true
filePath = await path.join(await path.desktopDir(), "some_folder", "non_existing_file.xlsx");
console.log(filePath, await exists(filePath, {baseDir: BaseDirectory.Desktop})); // false
```

### Expected behavior

No exception for this one:

```ts
let filePath = await path.join(await path.resourceDir(), "some_folder", "non_existing_file.xlsx"); // non-existing file
let fileExists = await exists(filePath);
```

Correct results for this one. Not that important, since the access technically shouldn't give correct results (you're appending the whole path to the base path), but it did work for the desktop directory, so...

```ts
let filePath = await path.join(await path.resourceDir(), "some_folder", "file.xlsx"); // either existing or non-existing file
let fileExists = await exists(filePath, {baseDir: BaseDirectory.Resource});
```



### Full `tauri info` output

```text
[✔] Environment
    - OS: Windows 10.0.22621 x86_64 (X64)
    ✔ WebView2: 130.0.2849.56
    ✔ MSVC: Visual Studio Community 2022
    ✔ rustc: 1.82.0 (f6e511eec 2024-10-15)
    ✔ cargo: 1.82.0 (8f40fc59f 2024-08-21)
    ✔ rustup: 1.27.1 (54dd3d00f 2024-04-24)
    ✔ Rust toolchain: stable-x86_64-pc-windows-msvc (default)
    - node: 20.11.1
    - pnpm: 8.15.5
    - yarn: 1.22.21
    - npm: 10.5.0

[-] Packages
    - tauri 🦀: 2.0.6
    - tauri-build 🦀: 2.0.2
    - wry 🦀: 0.46.3
    - tao 🦀: 0.30.5
    - @tauri-apps/api : 2.0.3
    - @tauri-apps/cli : 2.0.5

[-] Plugins
    - tauri-plugin-fs 🦀: 2.0.3
    - @tauri-apps/plugin-fs : 2.0.2
    - tauri-plugin-log 🦀: 2.0.2
    - @tauri-apps/plugin-log : 2.0.0
    - tauri-plugin-window-state 🦀: 2.0.2
    - @tauri-apps/plugin-window-state : 2.0.0
    - tauri-plugin-dialog 🦀: 2.0.3
    - @tauri-apps/plugin-dialog : 2.0.1
    - tauri-plugin-shell 🦀: 2.0.2
    - @tauri-apps/plugin-shell : 2.0.1

[-] App
    - build-type: bundle
    - CSP: unset
    - frontendDist: ../dist
    - devUrl: http://localhost:1420/
    - framework: Vue.js
    - bundler: Vite
```


### Stack trace

_No response_

### Additional context

_No response_


Discussion / hints:

Fix patch:
diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs
index 81f0eaae284d..08c26b242fbf 100644
--- a/crates/tauri/src/manager/mod.rs
+++ b/crates/tauri/src/manager/mod.rs
@@ -376,7 +376,7 @@ impl<R: Runtime> AppManager<R> {
   pub fn get_asset(
     &self,
     mut path: String,
-    use_https_schema: bool,
+    _use_https_schema: bool,
   ) -> Result<Asset, Box<dyn std::error::Error>> {
     let assets = &self.assets;
     if path.ends_with('/') {
@@ -440,7 +440,10 @@ impl<R: Runtime> AppManager<R> {
               let default_src = csp_map
                 .entry("default-src".into())
                 .or_insert_with(Default::default);
-              default_src.push(crate::pattern::format_real_schema(schema, use_https_schema));
+              default_src.push(crate::pattern::format_real_schema(
+                schema,
+                _use_https_schema,
+              ));
             }
 
             csp_header.replace(Csp::DirectiveMap(csp_map).to_string());
diff --git a/crates/tauri/src/menu/mod.rs b/crates/tauri/src/menu/mod.rs
index 2ae33d971b89..cee643ea2ea4 100644
--- a/crates/tauri/src/menu/mod.rs
+++ b/crates/tauri/src/menu/mod.rs
@@ -99,7 +99,7 @@ macro_rules! gen_wrappers {
           // SAFETY: inner was created on main thread and is being dropped on main thread
           let inner = $crate::UnsafeSend(inner);
           let _ = self.app_handle.run_on_main_thread(move || {
-            drop(inner);
+            drop(inner.take());
           });
         }
       }
diff --git a/crates/tauri/src/scope/fs.rs b/crates/tauri/src/scope/fs.rs
index 0ef56ad8070a..6fafe6578719 100644
--- a/crates/tauri/src/scope/fs.rs
+++ b/crates/tauri/src/scope/fs.rs
@@ -92,10 +92,27 @@ fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternErro
   //   - `C:\\SomeDir`
   #[cfg(windows)]
   {
-    use std::path::Component;
+    use std::path::{Component, Path, Prefix};
+
+    let mut components = path.components();
+
+    let is_unc = match components.next() {
+      Some(Component::Prefix(p)) => match p.kind() {
+        Prefix::VerbatimDisk(..) => true,
+        _ => false, // Other kinds of UNC paths
+      },
+      _ => false, // relative or empty
+    };
+
+    if is_unc {
+      // we remove UNC manually, instead of `dunce::simplified` because
+      // `path` could have `*` in it and that's not allowed on Windows and
+      // `dunce::simplified` will check that and return `path` as is without simplification
+      let simplified = path
+        .to_str()
+        .and_then(|s| s.get(4..))
+        .map_or(path.as_path(), Path::new);
 
-    if matches!(path.components().next(), Some(Component::Prefix(_))) {
-      let simplified = dunce::simplified(&path);
       let simplified_str = simplified.to_string_lossy();
       if simplified_str != path_str {
         list.insert(f(&simplified_str)?);
@@ -356,6 +373,7 @@ impl Scope {
           .unwrap()
           .iter()
           .any(|p| p.matches_path_with(&path, self.match_options));
+
         allowed
       }
     } else {
@@ -382,7 +400,11 @@ fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternE
 
 #[cfg(test)]
 mod tests {
-  use super::Scope;
+  use std::collections::HashSet;
+
+  use glob::Pattern;
+
+  use super::{push_pattern, Scope};
 
   fn new_scope() -> Scope {
     Scope {
@@ -600,4 +622,37 @@ mod tests {
       assert!(!scope.is_allowed("Q:Cargo.toml"));
     }
   }
+
+  #[test]
+  fn push_pattern_generated_paths() {
+    macro_rules! assert_pattern {
+      ($patterns:ident, $pattern:literal) => {
+        assert!($patterns.contains(&Pattern::new($pattern).unwrap()))
+      };
+    }
+
+    let mut patterns = HashSet::new();
+
+    #[cfg(not(windows))]
+    {
+      push_pattern(&mut patterns, "/path/to/dir/", Pattern::new).expect("failed to push pattern");
+      push_pattern(&mut patterns, "/path/to/dir/**", Pattern::new).expect("failed to push pattern");
+
+      assert_pattern!(patterns, "/path/to/dir");
+      assert_pattern!(patterns, "/path/to/dir/**");
+    }
+
+    #[cfg(windows)]
+    {
+      push_pattern(&mut patterns, "C:\\path\\to\\dir", Pattern::new)
+        .expect("failed to push pattern");
+      push_pattern(&mut patterns, "C:\\path\\to\\dir\\**", Pattern::new)
+        .expect("failed to push pattern");
+
+      assert_pattern!(patterns, "C:\\path\\to\\dir");
+      assert_pattern!(patterns, "C:\\path\\to\\dir\\**");
+      assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir");
+      assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**");
+    }
+  }
 }


Test patch: