path.rs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use crate::{api::path::BaseDirectory, Runtime};
  5. #[cfg(path_all)]
  6. use crate::{Env, Manager};
  7. use std::path::PathBuf;
  8. #[cfg(path_all)]
  9. use std::path::{Component, Path, MAIN_SEPARATOR};
  10. use super::InvokeContext;
  11. use serde::Deserialize;
  12. use tauri_macros::{module_command_handler, CommandModule};
  13. /// The API descriptor.
  14. #[derive(Deserialize, CommandModule)]
  15. #[serde(tag = "cmd", rename_all = "camelCase")]
  16. pub enum Cmd {
  17. ResolvePath {
  18. path: String,
  19. directory: Option<BaseDirectory>,
  20. },
  21. Resolve {
  22. paths: Vec<String>,
  23. },
  24. Normalize {
  25. path: String,
  26. },
  27. Join {
  28. paths: Vec<String>,
  29. },
  30. Dirname {
  31. path: String,
  32. },
  33. Extname {
  34. path: String,
  35. },
  36. Basename {
  37. path: String,
  38. ext: Option<String>,
  39. },
  40. IsAbsolute {
  41. path: String,
  42. },
  43. }
  44. impl Cmd {
  45. #[module_command_handler(path_all, "path > all")]
  46. fn resolve_path<R: Runtime>(
  47. context: InvokeContext<R>,
  48. path: String,
  49. directory: Option<BaseDirectory>,
  50. ) -> crate::Result<PathBuf> {
  51. crate::api::path::resolve_path(
  52. &context.config,
  53. &context.package_info,
  54. context.window.state::<Env>().inner(),
  55. path,
  56. directory,
  57. )
  58. .map_err(Into::into)
  59. }
  60. #[module_command_handler(path_all, "path > all")]
  61. fn resolve<R: Runtime>(_context: InvokeContext<R>, paths: Vec<String>) -> crate::Result<PathBuf> {
  62. // Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which
  63. // will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path.
  64. //
  65. // examples:
  66. // 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()`
  67. // 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")`
  68. let mut path = std::env::current_dir()?;
  69. for p in paths {
  70. path.push(p);
  71. }
  72. Ok(normalize_path(&path))
  73. }
  74. #[module_command_handler(path_all, "path > all")]
  75. fn normalize<R: Runtime>(_context: InvokeContext<R>, path: String) -> crate::Result<String> {
  76. let mut p = normalize_path_no_absolute(Path::new(&path))
  77. .to_string_lossy()
  78. .to_string();
  79. Ok(
  80. // Node.js behavior is to return `".."` for `normalize("..")`
  81. // and `"."` for `normalize("")` or `normalize(".")`
  82. if p.is_empty() && path == ".." {
  83. "..".into()
  84. } else if p.is_empty() && path == "." {
  85. ".".into()
  86. } else {
  87. // Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves.
  88. if (path.ends_with('/') || path.ends_with('\\'))
  89. && (!p.ends_with('/') || !p.ends_with('\\'))
  90. {
  91. p.push(MAIN_SEPARATOR);
  92. }
  93. p
  94. },
  95. )
  96. }
  97. #[module_command_handler(path_all, "path > all")]
  98. fn join<R: Runtime>(_context: InvokeContext<R>, mut paths: Vec<String>) -> crate::Result<String> {
  99. let path = PathBuf::from(
  100. paths
  101. .iter_mut()
  102. .map(|p| {
  103. // Add a `MAIN_SEPARATOR` if it doesn't already have one.
  104. // Doing this to ensure that the vector elements are separated in
  105. // the resulting string so path.components() can work correctly when called
  106. // in `normalize_path_no_absolute()` later on.
  107. if !p.ends_with('/') && !p.ends_with('\\') {
  108. p.push(MAIN_SEPARATOR);
  109. }
  110. p.to_string()
  111. })
  112. .collect::<String>(),
  113. );
  114. let p = normalize_path_no_absolute(&path)
  115. .to_string_lossy()
  116. .to_string();
  117. Ok(if p.is_empty() { ".".into() } else { p })
  118. }
  119. #[module_command_handler(path_all, "path > all")]
  120. fn dirname<R: Runtime>(_context: InvokeContext<R>, path: String) -> crate::Result<PathBuf> {
  121. match Path::new(&path).parent() {
  122. Some(p) => Ok(p.to_path_buf()),
  123. None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path(
  124. "Couldn't get the parent directory".into(),
  125. ))),
  126. }
  127. }
  128. #[module_command_handler(path_all, "path > all")]
  129. fn extname<R: Runtime>(_context: InvokeContext<R>, path: String) -> crate::Result<String> {
  130. match Path::new(&path)
  131. .extension()
  132. .and_then(std::ffi::OsStr::to_str)
  133. {
  134. Some(p) => Ok(p.to_string()),
  135. None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path(
  136. "Couldn't get the extension of the file".into(),
  137. ))),
  138. }
  139. }
  140. #[module_command_handler(path_all, "path > all")]
  141. fn basename<R: Runtime>(
  142. _context: InvokeContext<R>,
  143. path: String,
  144. ext: Option<String>,
  145. ) -> crate::Result<String> {
  146. match Path::new(&path)
  147. .file_name()
  148. .and_then(std::ffi::OsStr::to_str)
  149. {
  150. Some(p) => Ok(if let Some(ext) = ext {
  151. p.replace(ext.as_str(), "")
  152. } else {
  153. p.to_string()
  154. }),
  155. None => Err(crate::Error::FailedToExecuteApi(crate::api::Error::Path(
  156. "Couldn't get the basename".into(),
  157. ))),
  158. }
  159. }
  160. #[module_command_handler(path_all, "path > all")]
  161. fn is_absolute<R: Runtime>(_context: InvokeContext<R>, path: String) -> crate::Result<bool> {
  162. Ok(Path::new(&path).is_absolute())
  163. }
  164. }
  165. /// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util.
  166. /// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106
  167. #[cfg(path_all)]
  168. fn normalize_path(path: &Path) -> PathBuf {
  169. let mut components = path.components().peekable();
  170. let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
  171. components.next();
  172. PathBuf::from(c.as_os_str())
  173. } else {
  174. PathBuf::new()
  175. };
  176. for component in components {
  177. match component {
  178. Component::Prefix(..) => unreachable!(),
  179. Component::RootDir => {
  180. ret.push(component.as_os_str());
  181. }
  182. Component::CurDir => {}
  183. Component::ParentDir => {
  184. ret.pop();
  185. }
  186. Component::Normal(c) => {
  187. ret.push(c);
  188. }
  189. }
  190. }
  191. ret
  192. }
  193. /// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but
  194. /// slightly modified to not resolve absolute paths.
  195. /// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106
  196. #[cfg(path_all)]
  197. fn normalize_path_no_absolute(path: &Path) -> PathBuf {
  198. let mut components = path.components().peekable();
  199. let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
  200. components.next();
  201. PathBuf::from(c.as_os_str())
  202. } else {
  203. PathBuf::new()
  204. };
  205. for component in components {
  206. match component {
  207. Component::Prefix(..) => unreachable!(),
  208. Component::RootDir => {
  209. ret.push(component.as_os_str());
  210. }
  211. Component::CurDir => {}
  212. Component::ParentDir => {
  213. ret.pop();
  214. }
  215. Component::Normal(c) => {
  216. // Using PathBuf::push here will replace the whole path if an absolute path is encountered
  217. // which is not the intended behavior, so instead of that, convert the current resolved path
  218. // to a string and do simple string concatenation with the current component then convert it
  219. // back to a PathBuf
  220. let mut p = ret.to_string_lossy().to_string();
  221. // Only add a separator if it doesn't have one already or if current normalized path is empty,
  222. // this ensures it won't have an unwanted leading separator
  223. if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
  224. p.push(MAIN_SEPARATOR);
  225. }
  226. if let Some(c) = c.to_str() {
  227. p.push_str(c);
  228. }
  229. ret = PathBuf::from(p);
  230. }
  231. }
  232. }
  233. ret
  234. }