tauri_plugin_android_fs/api/models/
fs_uri.rs1use crate::*;
2use serde::{Deserialize, Serialize};
3
4#[deprecated = "Use `tauri_plugin_android_fs::FsUri` instead."]
5pub type FileUri = FsUri;
6
7#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct FsUri {
21
22 pub uri: String,
27
28 pub document_top_tree_uri: Option<String>,
34}
35
36impl FsUri {
37
38 pub fn to_json_string(&self) -> Result<String> {
40 serde_json::to_string(self).map_err(Into::into)
41 }
42
43 pub fn from_json_str(json: impl AsRef<str>) -> Result<Self> {
45 serde_json::from_str(json.as_ref()).map_err(Into::into)
46 }
47
48 pub fn from_uri(uri: impl Into<String>) -> Self {
49 FsUri {
50 uri: uri.into(),
51 document_top_tree_uri: None,
52 }
53 }
54
55 pub fn from_path(path: impl AsRef<std::path::Path>) -> Self {
66 Self {
67 uri: path_to_android_file_uri(path),
68 document_top_tree_uri: None,
69 }
70 }
71
72 pub fn to_path(&self) -> Option<std::path::PathBuf> {
75 if self.is_file_scheme() {
76 return Some(android_file_uri_to_path(&self.uri));
77 }
78 None
79 }
80
81 pub fn is_file_scheme(&self) -> bool {
83 self.uri.starts_with("file://")
84 }
85
86 pub fn is_content_scheme(&self) -> bool {
88 self.uri.starts_with("content://")
89 }
90}
91
92impl From<&std::path::Path> for FsUri {
93 fn from(path: &std::path::Path) -> Self {
94 Self::from_path(path)
95 }
96}
97
98impl From<&std::path::PathBuf> for FsUri {
99 fn from(path: &std::path::PathBuf) -> Self {
100 Self::from_path(path)
101 }
102}
103
104impl From<std::path::PathBuf> for FsUri {
105 fn from(path: std::path::PathBuf) -> Self {
106 Self::from_path(path)
107 }
108}
109
110impl From<tauri_plugin_fs::FilePath> for FsUri {
111 fn from(value: tauri_plugin_fs::FilePath) -> Self {
112 match value {
113 tauri_plugin_fs::FilePath::Url(url) => Self::from_uri(url),
114 tauri_plugin_fs::FilePath::Path(path) => Self::from_path(path),
115 }
116 }
117}
118
119impl From<FsUri> for tauri_plugin_fs::FilePath {
120 fn from(value: FsUri) -> Self {
121 type NeverErr<T> = std::result::Result<T, std::convert::Infallible>;
122 NeverErr::unwrap(value.uri.parse())
123 }
124}
125
126fn android_file_uri_to_path(uri: impl AsRef<str>) -> std::path::PathBuf {
127 let uri = uri.as_ref();
128 let path_part = uri.strip_prefix("file://").unwrap_or(uri);
129 let decoded = percent_encoding::percent_decode_str(path_part).decode_utf8_lossy();
130
131 std::path::PathBuf::from(decoded.as_ref())
132}
133
134fn path_to_android_file_uri(path: impl AsRef<std::path::Path>) -> String {
135 let encoded = path
136 .as_ref()
137 .to_string_lossy()
138 .split('/')
139 .map(|s| encode_android_uri_component(s))
140 .collect::<Vec<_>>()
141 .join("/");
142
143 format!("file://{}", encoded)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::path::Path;
150
151 #[test]
152 fn test_android_safe_characters() {
153 let path = Path::new("/sdcard/test_file-name!.~'()*.txt");
154 let uri = path_to_android_file_uri(path);
155
156 assert_eq!(uri, "file:///sdcard/test_file-name!.~'()*.txt");
157 assert_eq!(android_file_uri_to_path(&uri), path);
158 }
159
160 #[test]
161 fn test_spaces_and_unsafe_chars() {
162 let path = Path::new("/sdcard/My Documents/file @#$%.txt");
163 let uri = path_to_android_file_uri(path);
164
165 assert_eq!(uri, "file:///sdcard/My%20Documents/file%20%40%23%24%25.txt");
166 assert_eq!(android_file_uri_to_path(&uri), path);
167 }
168
169 #[test]
170 fn test_unicode_characters() {
171 let path = Path::new("/sdcard/ダウンロード");
172 let uri = path_to_android_file_uri(path);
173
174 assert_eq!(
175 uri,
176 "file:///sdcard/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89"
177 );
178 assert_eq!(android_file_uri_to_path(&uri), path);
179 }
180}