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