plugin.rs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
  5. use serialize_to_javascript::{default_template, DefaultTemplate, Template};
  6. use super::{BaseDirectory, Error, PathResolver, Result};
  7. use crate::{
  8. command,
  9. plugin::{Builder, TauriPlugin},
  10. AppHandle, Manager, Runtime, State,
  11. };
  12. /// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util.
  13. /// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106
  14. fn normalize_path(path: &Path) -> PathBuf {
  15. let mut components = path.components().peekable();
  16. let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
  17. components.next();
  18. PathBuf::from(c.as_os_str())
  19. } else {
  20. PathBuf::new()
  21. };
  22. for component in components {
  23. match component {
  24. Component::Prefix(..) => unreachable!(),
  25. Component::RootDir => {
  26. ret.push(component.as_os_str());
  27. }
  28. Component::CurDir => {}
  29. Component::ParentDir => {
  30. ret.pop();
  31. }
  32. Component::Normal(c) => {
  33. ret.push(c);
  34. }
  35. }
  36. }
  37. ret
  38. }
  39. /// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but
  40. /// slightly modified to not resolve absolute paths.
  41. /// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106
  42. fn normalize_path_no_absolute(path: &Path) -> PathBuf {
  43. let mut components = path.components().peekable();
  44. let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
  45. components.next();
  46. PathBuf::from(c.as_os_str())
  47. } else {
  48. PathBuf::new()
  49. };
  50. for component in components {
  51. match component {
  52. Component::Prefix(..) => unreachable!(),
  53. Component::RootDir => {
  54. ret.push(component.as_os_str());
  55. }
  56. Component::CurDir => {}
  57. Component::ParentDir => {
  58. ret.pop();
  59. }
  60. Component::Normal(c) => {
  61. // Using PathBuf::push here will replace the whole path if an absolute path is encountered
  62. // which is not the intended behavior, so instead of that, convert the current resolved path
  63. // to a string and do simple string concatenation with the current component then convert it
  64. // back to a PathBuf
  65. let mut p = ret.to_string_lossy().to_string();
  66. // Only add a separator if it doesn't have one already or if current normalized path is empty,
  67. // this ensures it won't have an unwanted leading separator
  68. if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
  69. p.push(MAIN_SEPARATOR);
  70. }
  71. if let Some(c) = c.to_str() {
  72. p.push_str(c);
  73. }
  74. ret = PathBuf::from(p);
  75. }
  76. }
  77. }
  78. ret
  79. }
  80. #[command(root = "crate")]
  81. pub fn resolve_directory<R: Runtime>(
  82. _app: AppHandle<R>,
  83. resolver: State<'_, PathResolver<R>>,
  84. directory: BaseDirectory,
  85. path: Option<PathBuf>,
  86. ) -> Result<PathBuf> {
  87. super::resolve_path(&resolver, directory, path).map(|p| dunce::simplified(&p).to_path_buf())
  88. }
  89. #[command(root = "crate")]
  90. pub fn resolve(paths: Vec<String>) -> Result<PathBuf> {
  91. // Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which
  92. // will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path.
  93. //
  94. // examples:
  95. // 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()`
  96. // 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")`
  97. let mut path = std::env::current_dir().map_err(Error::CurrentDir)?;
  98. for p in paths {
  99. path.push(p);
  100. }
  101. Ok(dunce::simplified(&normalize_path(&path)).to_path_buf())
  102. }
  103. #[command(root = "crate")]
  104. pub fn normalize(path: String) -> String {
  105. let mut p = dunce::simplified(&normalize_path_no_absolute(Path::new(&path)))
  106. .to_string_lossy()
  107. .to_string();
  108. // Node.js behavior is to return `".."` for `normalize("..")`
  109. // and `"."` for `normalize("")` or `normalize(".")`
  110. if p.is_empty() && path == ".." {
  111. "..".into()
  112. } else if p.is_empty() && path == "." {
  113. ".".into()
  114. } else {
  115. // Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves.
  116. if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) {
  117. p.push(MAIN_SEPARATOR);
  118. }
  119. p
  120. }
  121. }
  122. #[command(root = "crate")]
  123. pub fn join(mut paths: Vec<String>) -> String {
  124. let path = PathBuf::from(
  125. paths
  126. .iter_mut()
  127. .map(|p| {
  128. // Add a `MAIN_SEPARATOR` if it doesn't already have one.
  129. // Doing this to ensure that the vector elements are separated in
  130. // the resulting string so path.components() can work correctly when called
  131. // in `normalize_path_no_absolute()` later on.
  132. if !p.ends_with('/') && !p.ends_with('\\') {
  133. p.push(MAIN_SEPARATOR);
  134. }
  135. p.to_string()
  136. })
  137. .collect::<String>(),
  138. );
  139. let p = dunce::simplified(&normalize_path_no_absolute(&path))
  140. .to_string_lossy()
  141. .to_string();
  142. if p.is_empty() {
  143. ".".into()
  144. } else {
  145. p
  146. }
  147. }
  148. #[command(root = "crate")]
  149. pub fn dirname(path: String) -> Result<PathBuf> {
  150. match Path::new(&path).parent() {
  151. Some(p) => Ok(dunce::simplified(p).to_path_buf()),
  152. None => Err(Error::NoParent),
  153. }
  154. }
  155. #[command(root = "crate")]
  156. pub fn extname(path: String) -> Result<String> {
  157. match Path::new(&path)
  158. .extension()
  159. .and_then(std::ffi::OsStr::to_str)
  160. {
  161. Some(p) => Ok(p.to_string()),
  162. None => Err(Error::NoExtension),
  163. }
  164. }
  165. #[command(root = "crate")]
  166. pub fn basename(path: &str, ext: Option<&str>) -> Result<String> {
  167. let file_name = Path::new(path).file_name().map(|f| f.to_string_lossy());
  168. match file_name {
  169. Some(p) => {
  170. let maybe_stripped = if let Some(ext) = ext {
  171. p.strip_suffix(ext).unwrap_or(&p).to_string()
  172. } else {
  173. p.to_string()
  174. };
  175. Ok(maybe_stripped)
  176. }
  177. None => Err(Error::NoBasename),
  178. }
  179. }
  180. #[command(root = "crate")]
  181. pub fn is_absolute(path: String) -> bool {
  182. Path::new(&path).is_absolute()
  183. }
  184. #[derive(Template)]
  185. #[default_template("./init.js")]
  186. struct InitJavascript {
  187. sep: &'static str,
  188. delimiter: &'static str,
  189. }
  190. /// Initializes the plugin.
  191. pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
  192. #[cfg(windows)]
  193. let (sep, delimiter) = ("\\", ";");
  194. #[cfg(not(windows))]
  195. let (sep, delimiter) = ("/", ":");
  196. let init_js = InitJavascript { sep, delimiter }
  197. .render_default(&Default::default())
  198. // this will never fail with the above sep and delimiter values
  199. .unwrap();
  200. Builder::new("path")
  201. .invoke_handler(crate::generate_handler![
  202. resolve_directory,
  203. resolve,
  204. normalize,
  205. join,
  206. dirname,
  207. extname,
  208. basename,
  209. is_absolute
  210. ])
  211. .js_init_script(init_js.to_string())
  212. .setup(|app, _api| {
  213. #[cfg(target_os = "android")]
  214. {
  215. let handle = _api.register_android_plugin("app.tauri", "PathPlugin")?;
  216. app.manage(PathResolver(handle));
  217. }
  218. #[cfg(not(target_os = "android"))]
  219. {
  220. app.manage(PathResolver(app.clone()));
  221. }
  222. Ok(())
  223. })
  224. .build()
  225. }
  226. #[cfg(test)]
  227. mod tests {
  228. #[test]
  229. fn basename() {
  230. let path = "/path/to/some-json-file.json";
  231. assert_eq!(
  232. super::basename(path, Some(".json")).unwrap(),
  233. "some-json-file"
  234. );
  235. let path = "/path/to/some-json-file.json";
  236. assert_eq!(
  237. super::basename(path, Some("json")).unwrap(),
  238. "some-json-file."
  239. );
  240. let path = "/path/to/some-json-file.html.json";
  241. assert_eq!(
  242. super::basename(path, Some(".json")).unwrap(),
  243. "some-json-file.html"
  244. );
  245. let path = "/path/to/some-json-file.json.json";
  246. assert_eq!(
  247. super::basename(path, Some(".json")).unwrap(),
  248. "some-json-file.json"
  249. );
  250. let path = "/path/to/some-json-file.json.html";
  251. assert_eq!(
  252. super::basename(path, Some(".json")).unwrap(),
  253. "some-json-file.json.html"
  254. );
  255. }
  256. }