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