tauri_plugin_android_fs/api/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
86impl From<&std::path::Path> for FileUri {
87
88 fn from(path: &std::path::Path) -> Self {
89 Self::from_path(path)
90 }
91}
92
93impl From<&std::path::PathBuf> for FileUri {
94
95 fn from(path: &std::path::PathBuf) -> Self {
96 Self::from_path(path)
97 }
98}
99
100impl From<std::path::PathBuf> for FileUri {
101
102 fn from(path: std::path::PathBuf) -> Self {
103 Self::from_path(path)
104 }
105}
106
107impl From<tauri_plugin_fs::FilePath> for FileUri {
108
109 fn from(value: tauri_plugin_fs::FilePath) -> Self {
110 match value {
111 tauri_plugin_fs::FilePath::Url(url) => Self::from_uri(url),
112 tauri_plugin_fs::FilePath::Path(path) => Self::from_path(path),
113 }
114 }
115}
116
117impl From<FileUri> for tauri_plugin_fs::FilePath {
118
119 fn from(value: FileUri) -> Self {
120 type NeverErr<T> = std::result::Result::<T, std::convert::Infallible>;
121 NeverErr::unwrap(value.uri.parse())
122 }
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)
130 .decode_utf8_lossy();
131
132 std::path::PathBuf::from(decoded.as_ref())
133}
134
135fn path_to_android_file_uri(path: impl AsRef<std::path::Path>) -> String {
136 let encoded = path
137 .as_ref()
138 .to_string_lossy()
139 .split('/')
140 .map(|s| encode_android_uri_component(s))
141 .collect::<Vec<_>>()
142 .join("/");
143
144 format!("file://{}", encoded)
145}
146
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use std::path::Path;
152
153 #[test]
154 fn test_android_safe_characters() {
155 let path = Path::new("/sdcard/test_file-name!.~'()*.txt");
156 let uri = path_to_android_file_uri(path);
157
158 assert_eq!(uri, "file:///sdcard/test_file-name!.~'()*.txt");
159 assert_eq!(android_file_uri_to_path(&uri), path);
160 }
161
162 #[test]
163 fn test_spaces_and_unsafe_chars() {
164 let path = Path::new("/sdcard/My Documents/file @#$%.txt");
165 let uri = path_to_android_file_uri(path);
166
167 assert_eq!(uri, "file:///sdcard/My%20Documents/file%20%40%23%24%25.txt");
168 assert_eq!(android_file_uri_to_path(&uri), path);
169 }
170
171 #[test]
172 fn test_unicode_characters() {
173 let path = Path::new("/sdcard/ダウンロード");
174 let uri = path_to_android_file_uri(path);
175
176 assert_eq!(uri, "file:///sdcard/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89");
177 assert_eq!(android_file_uri_to_path(&uri), path);
178 }
179}