Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/security-scoped-ios.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"fs": minor
"fs-js": minor
---

Enable access for security-scoped resources on iOS by automatically calling `NSURL::startAccessingSecurityScopedResource` on resource access and adding the `stopAccessingSecurityScopedResource` API.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions plugins/fs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ serde_repr = "0.1"
tauri = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
log = { workspace = true }
anyhow = "1"
glob = { workspace = true }
# TODO: Remove `serialization-compat-6` in v3
Expand All @@ -41,5 +42,8 @@ notify-debouncer-full = { version = "0.6", optional = true }
dunce = { workspace = true }
percent-encoding = "2"

[target.'cfg(target_os = "ios")'.dependencies]
objc2-foundation = { version = "0.3", features = ["NSURL", "NSString"] }

[features]
watch = ["notify", "notify-debouncer-full"]
2 changes: 1 addition & 1 deletion plugins/fs/api-iife.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions plugins/fs/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ const COMMANDS: &[(&str, &[&str])] = &[
// TODO: Remove this in v3
("unwatch", &[]),
("size", &[]),
("start_accessing_security_scoped_resource", &[]),
("stop_accessing_security_scoped_resource", &[]),
];

fn main() {
Expand Down
90 changes: 89 additions & 1 deletion plugins/fs/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
/**
* Access the file system.
*
* ## iOS security-scoped resources
*
* On iOS, the `fs` plugin automatically manages access to security-scoped resources when a file URL is accessed.
* This is required for files outside the app's sandbox (e.g., from file picker).
*
* @example
* ```typescript
* import { open } from '@tauri-apps/plugin-fs';
*
* const file = await open('file:///path/to/file.txt');
* await file.close();
* ```
*
* ## Security
*
* This module prevents path traversal, not allowing parent directory accessors to be used
Expand Down Expand Up @@ -1353,6 +1366,79 @@ async function size(path: string | URL): Promise<number> {
})
}

/**
* Starts accessing a security-scoped resource for the given file URL.
* This should be called when you're accessing a file that was opened
* using a security-scoped URL (e.g., from a file picker).
*
* Note that accessing security-scoped resources is automatically managed by the plugin on iOS, so you don't need to call this function
* unless you want to manage the scope manually.
*
* You must call {@linkcode stopAccessingSecurityScopedResource} when you're done accessing the resource.
*
* #### Platform-specific
*
* - **iOS:** Starts accessing the security-scoped resource.
* - **Other platforms:** does nothing.
*
* @example
* ```typescript
* import { startAccessingSecurityScopedResource } from '@tauri-apps/plugin-fs';
*
* const filePath = 'file:///path/to/file.txt';
* await startAccessingSecurityScopedResource(filePath);
* // ... use the resource ...
* ```
*
* @since 2.5.0
*/
async function startAccessingSecurityScopedResource(
path: string | URL
): Promise<void> {
if (path instanceof URL && path.protocol !== 'file:') {
throw new TypeError('Must be a file URL.')
}

await invoke('plugin:fs|start_accessing_security_scoped_resource', {
path: path instanceof URL ? path.toString() : path
})
}

/**
* Stops accessing a security-scoped resource for the given file URL.
* This should be called when you're done accessing a file that was opened
* using a security-scoped URL (e.g., from a file picker) when using manual tracking via {@linkcode startAccessingSecurityScopedResource}.
*
* #### Platform-specific
*
* - **iOS:** Stops accessing the security-scoped resource.
* - **Other platforms:** does nothing.
*
* @example
* ```typescript
* import { stopAccessingSecurityScopedResource } from '@tauri-apps/plugin-fs';
*
* const filePath = 'file:///path/to/file.txt';
* await startAccessingSecurityScopedResource(filePath);
* // ... use the resource ...
* // when you're done with the resource:
* await stopAccessingSecurityScopedResource(filePath);
* ```
*
* @since 2.5.0
*/
async function stopAccessingSecurityScopedResource(
path: string | URL
): Promise<void> {
if (path instanceof URL && path.protocol !== 'file:') {
throw new TypeError('Must be a file URL.')
}

await invoke('plugin:fs|stop_accessing_security_scoped_resource', {
path: path instanceof URL ? path.toString() : path
})
}

export type {
CreateOptions,
OpenOptions,
Expand Down Expand Up @@ -1401,5 +1487,7 @@ export {
exists,
watch,
watchImmediate,
size
size,
startAccessingSecurityScopedResource,
stopAccessingSecurityScopedResource
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!

"$schema" = "../../schemas/schema.json"

[[permission]]
identifier = "allow-start-accessing-security-scoped-resource"
description = "Enables the start_accessing_security_scoped_resource command without any pre-configured scope."
commands.allow = ["start_accessing_security_scoped_resource"]

[[permission]]
identifier = "deny-start-accessing-security-scoped-resource"
description = "Denies the start_accessing_security_scoped_resource command without any pre-configured scope."
commands.deny = ["start_accessing_security_scoped_resource"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!

"$schema" = "../../schemas/schema.json"

[[permission]]
identifier = "allow-stop-accessing-security-scoped-resource"
description = "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope."
commands.allow = ["stop_accessing_security_scoped_resource"]

[[permission]]
identifier = "deny-stop-accessing-security-scoped-resource"
description = "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope."
commands.deny = ["stop_accessing_security_scoped_resource"]
52 changes: 52 additions & 0 deletions plugins/fs/permissions/autogenerated/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3435,6 +3435,32 @@ Denies the size command without any pre-configured scope.
<tr>
<td>

`fs:allow-start-accessing-security-scoped-resource`

</td>
<td>

Enables the start_accessing_security_scoped_resource command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`fs:deny-start-accessing-security-scoped-resource`

</td>
<td>

Denies the start_accessing_security_scoped_resource command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`fs:allow-stat`

</td>
Expand All @@ -3461,6 +3487,32 @@ Denies the stat command without any pre-configured scope.
<tr>
<td>

`fs:allow-stop-accessing-security-scoped-resource`

</td>
<td>

Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`fs:deny-stop-accessing-security-scoped-resource`

</td>
<td>

Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`fs:allow-truncate`

</td>
Expand Down
24 changes: 24 additions & 0 deletions plugins/fs/permissions/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1860,6 +1860,18 @@
"const": "deny-size",
"markdownDescription": "Denies the size command without any pre-configured scope."
},
{
"description": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "allow-start-accessing-security-scoped-resource",
"markdownDescription": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "deny-start-accessing-security-scoped-resource",
"markdownDescription": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Enables the stat command without any pre-configured scope.",
"type": "string",
Expand All @@ -1872,6 +1884,18 @@
"const": "deny-stat",
"markdownDescription": "Denies the stat command without any pre-configured scope."
},
{
"description": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "allow-stop-accessing-security-scoped-resource",
"markdownDescription": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "deny-stop-accessing-security-scoped-resource",
"markdownDescription": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Enables the truncate command without any pre-configured scope.",
"type": "string",
Expand Down
54 changes: 22 additions & 32 deletions plugins/fs/src/mobile.rs → plugins/fs/src/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,31 @@
// SPDX-License-Identifier: MIT

use serde::de::DeserializeOwned;
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use tauri::{plugin::PluginApi, AppHandle, Runtime};

use crate::{models::*, FilePath, OpenOptions};

#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "com.plugin.fs";

#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_fs);
pub struct Fs<R: Runtime>(tauri::plugin::PluginHandle<R>);

// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Fs<R>> {
#[cfg(target_os = "android")]
let handle = api
.register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin")
.unwrap();
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_android - intent - send)?;
Ok(Fs(handle))
}

/// Access to the android-intent-send APIs.
pub struct Fs<R: Runtime>(PluginHandle<R>);

impl<R: Runtime> Fs<R> {
/// Open a file.
///
/// # Platform-specific
///
/// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL.
/// You must call `stop_accessing_security_scoped_resource` when you're done accessing the file.
pub fn open<P: Into<FilePath>>(
&self,
path: P,
Expand Down Expand Up @@ -68,29 +62,25 @@ impl<R: Runtime> Fs<R> {
}
}

#[cfg(target_os = "android")]
fn resolve_content_uri(
&self,
uri: impl Into<String>,
mode: impl Into<String>,
) -> crate::Result<std::fs::File> {
#[cfg(target_os = "android")]
{
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
unimplemented!()
}
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
unimplemented!()
}
}
}
Loading
Loading