file_system.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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::{
  5. api::{dir, file, path::BaseDirectory},
  6. scope::Scopes,
  7. Config, Env, Manager, PackageInfo, Runtime, Window,
  8. };
  9. use super::InvokeContext;
  10. use serde::{
  11. de::{Deserializer, Error as DeError},
  12. Deserialize, Serialize,
  13. };
  14. use tauri_macros::{module_command_handler, CommandModule};
  15. use std::{
  16. fs,
  17. fs::File,
  18. io::Write,
  19. path::{Component, Path},
  20. sync::Arc,
  21. };
  22. pub struct SafePathBuf(std::path::PathBuf);
  23. impl AsRef<Path> for SafePathBuf {
  24. fn as_ref(&self) -> &Path {
  25. self.0.as_ref()
  26. }
  27. }
  28. impl<'de> Deserialize<'de> for SafePathBuf {
  29. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  30. where
  31. D: Deserializer<'de>,
  32. {
  33. let path = std::path::PathBuf::deserialize(deserializer)?;
  34. if path.components().any(|x| {
  35. matches!(
  36. x,
  37. Component::ParentDir | Component::RootDir | Component::Prefix(_)
  38. )
  39. }) {
  40. Err(DeError::custom("cannot traverse directory"))
  41. } else {
  42. Ok(SafePathBuf(path))
  43. }
  44. }
  45. }
  46. /// The options for the directory functions on the file system API.
  47. #[derive(Debug, Clone, Deserialize)]
  48. pub struct DirOperationOptions {
  49. /// Whether the API should recursively perform the operation on the directory.
  50. #[serde(default)]
  51. pub recursive: bool,
  52. /// The base directory of the operation.
  53. /// The directory path of the BaseDirectory will be the prefix of the defined directory path.
  54. pub dir: Option<BaseDirectory>,
  55. }
  56. /// The options for the file functions on the file system API.
  57. #[derive(Debug, Clone, Deserialize)]
  58. pub struct FileOperationOptions {
  59. /// The base directory of the operation.
  60. /// The directory path of the BaseDirectory will be the prefix of the defined file path.
  61. pub dir: Option<BaseDirectory>,
  62. }
  63. /// The API descriptor.
  64. #[derive(Deserialize, CommandModule)]
  65. #[serde(tag = "cmd", rename_all = "camelCase")]
  66. pub enum Cmd {
  67. /// The read text file API.
  68. ReadFile {
  69. path: SafePathBuf,
  70. options: Option<FileOperationOptions>,
  71. },
  72. /// The write file API.
  73. WriteFile {
  74. path: SafePathBuf,
  75. contents: Vec<u8>,
  76. options: Option<FileOperationOptions>,
  77. },
  78. /// The read dir API.
  79. ReadDir {
  80. path: SafePathBuf,
  81. options: Option<DirOperationOptions>,
  82. },
  83. /// The copy file API.
  84. CopyFile {
  85. source: SafePathBuf,
  86. destination: SafePathBuf,
  87. options: Option<FileOperationOptions>,
  88. },
  89. /// The create dir API.
  90. CreateDir {
  91. path: SafePathBuf,
  92. options: Option<DirOperationOptions>,
  93. },
  94. /// The remove dir API.
  95. RemoveDir {
  96. path: SafePathBuf,
  97. options: Option<DirOperationOptions>,
  98. },
  99. /// The remove file API.
  100. RemoveFile {
  101. path: SafePathBuf,
  102. options: Option<FileOperationOptions>,
  103. },
  104. /// The rename file API.
  105. #[serde(rename_all = "camelCase")]
  106. RenameFile {
  107. old_path: SafePathBuf,
  108. new_path: SafePathBuf,
  109. options: Option<FileOperationOptions>,
  110. },
  111. }
  112. impl Cmd {
  113. #[module_command_handler(fs_read_file, "fs > readFile")]
  114. fn read_file<R: Runtime>(
  115. context: InvokeContext<R>,
  116. path: SafePathBuf,
  117. options: Option<FileOperationOptions>,
  118. ) -> crate::Result<Vec<u8>> {
  119. file::read_binary(resolve_path(
  120. &context.config,
  121. &context.package_info,
  122. &context.window,
  123. path,
  124. options.and_then(|o| o.dir),
  125. )?)
  126. .map_err(crate::Error::FailedToExecuteApi)
  127. }
  128. #[module_command_handler(fs_write_file, "fs > writeFile")]
  129. fn write_file<R: Runtime>(
  130. context: InvokeContext<R>,
  131. path: SafePathBuf,
  132. contents: Vec<u8>,
  133. options: Option<FileOperationOptions>,
  134. ) -> crate::Result<()> {
  135. File::create(resolve_path(
  136. &context.config,
  137. &context.package_info,
  138. &context.window,
  139. path,
  140. options.and_then(|o| o.dir),
  141. )?)
  142. .map_err(Into::into)
  143. .and_then(|mut f| f.write_all(&contents).map_err(|err| err.into()))
  144. }
  145. #[module_command_handler(fs_read_dir, "fs > readDir")]
  146. fn read_dir<R: Runtime>(
  147. context: InvokeContext<R>,
  148. path: SafePathBuf,
  149. options: Option<DirOperationOptions>,
  150. ) -> crate::Result<Vec<dir::DiskEntry>> {
  151. let (recursive, dir) = if let Some(options_value) = options {
  152. (options_value.recursive, options_value.dir)
  153. } else {
  154. (false, None)
  155. };
  156. dir::read_dir(
  157. resolve_path(
  158. &context.config,
  159. &context.package_info,
  160. &context.window,
  161. path,
  162. dir,
  163. )?,
  164. recursive,
  165. )
  166. .map_err(crate::Error::FailedToExecuteApi)
  167. }
  168. #[module_command_handler(fs_copy_file, "fs > copyFile")]
  169. fn copy_file<R: Runtime>(
  170. context: InvokeContext<R>,
  171. source: SafePathBuf,
  172. destination: SafePathBuf,
  173. options: Option<FileOperationOptions>,
  174. ) -> crate::Result<()> {
  175. let (src, dest) = match options.and_then(|o| o.dir) {
  176. Some(dir) => (
  177. resolve_path(
  178. &context.config,
  179. &context.package_info,
  180. &context.window,
  181. source,
  182. Some(dir.clone()),
  183. )?,
  184. resolve_path(
  185. &context.config,
  186. &context.package_info,
  187. &context.window,
  188. destination,
  189. Some(dir),
  190. )?,
  191. ),
  192. None => (source, destination),
  193. };
  194. fs::copy(src, dest)?;
  195. Ok(())
  196. }
  197. #[module_command_handler(fs_create_dir, "fs > createDir")]
  198. fn create_dir<R: Runtime>(
  199. context: InvokeContext<R>,
  200. path: SafePathBuf,
  201. options: Option<DirOperationOptions>,
  202. ) -> crate::Result<()> {
  203. let (recursive, dir) = if let Some(options_value) = options {
  204. (options_value.recursive, options_value.dir)
  205. } else {
  206. (false, None)
  207. };
  208. let resolved_path = resolve_path(
  209. &context.config,
  210. &context.package_info,
  211. &context.window,
  212. path,
  213. dir,
  214. )?;
  215. if recursive {
  216. fs::create_dir_all(resolved_path)?;
  217. } else {
  218. fs::create_dir(resolved_path)?;
  219. }
  220. Ok(())
  221. }
  222. #[module_command_handler(fs_remove_dir, "fs > removeDir")]
  223. fn remove_dir<R: Runtime>(
  224. context: InvokeContext<R>,
  225. path: SafePathBuf,
  226. options: Option<DirOperationOptions>,
  227. ) -> crate::Result<()> {
  228. let (recursive, dir) = if let Some(options_value) = options {
  229. (options_value.recursive, options_value.dir)
  230. } else {
  231. (false, None)
  232. };
  233. let resolved_path = resolve_path(
  234. &context.config,
  235. &context.package_info,
  236. &context.window,
  237. path,
  238. dir,
  239. )?;
  240. if recursive {
  241. fs::remove_dir_all(resolved_path)?;
  242. } else {
  243. fs::remove_dir(resolved_path)?;
  244. }
  245. Ok(())
  246. }
  247. #[module_command_handler(fs_remove_file, "fs > removeFile")]
  248. fn remove_file<R: Runtime>(
  249. context: InvokeContext<R>,
  250. path: SafePathBuf,
  251. options: Option<FileOperationOptions>,
  252. ) -> crate::Result<()> {
  253. let resolved_path = resolve_path(
  254. &context.config,
  255. &context.package_info,
  256. &context.window,
  257. path,
  258. options.and_then(|o| o.dir),
  259. )?;
  260. fs::remove_file(resolved_path)?;
  261. Ok(())
  262. }
  263. #[module_command_handler(fs_rename_file, "fs > renameFile")]
  264. fn rename_file<R: Runtime>(
  265. context: InvokeContext<R>,
  266. old_path: SafePathBuf,
  267. new_path: SafePathBuf,
  268. options: Option<FileOperationOptions>,
  269. ) -> crate::Result<()> {
  270. let (old, new) = match options.and_then(|o| o.dir) {
  271. Some(dir) => (
  272. resolve_path(
  273. &context.config,
  274. &context.package_info,
  275. &context.window,
  276. old_path,
  277. Some(dir.clone()),
  278. )?,
  279. resolve_path(
  280. &context.config,
  281. &context.package_info,
  282. &context.window,
  283. new_path,
  284. Some(dir),
  285. )?,
  286. ),
  287. None => (old_path, new_path),
  288. };
  289. fs::rename(old, new).map_err(crate::Error::Io)
  290. }
  291. }
  292. #[allow(dead_code)]
  293. fn resolve_path<R: Runtime>(
  294. config: &Config,
  295. package_info: &PackageInfo,
  296. window: &Window<R>,
  297. path: SafePathBuf,
  298. dir: Option<BaseDirectory>,
  299. ) -> crate::Result<SafePathBuf> {
  300. let env = window.state::<Env>().inner();
  301. match crate::api::path::resolve_path(config, package_info, env, path, dir) {
  302. Ok(path) => {
  303. if window.state::<Scopes>().fs.is_allowed(&path) {
  304. Ok(SafePathBuf(path))
  305. } else {
  306. Err(crate::Error::PathNotAllowed(path))
  307. }
  308. }
  309. Err(e) => Err(e.into()),
  310. }
  311. }
  312. #[cfg(test)]
  313. mod tests {
  314. use std::path::SafePathBuf;
  315. use super::{BaseDirectory, DirOperationOptions, FileOperationOptions};
  316. use quickcheck::{Arbitrary, Gen};
  317. impl Arbitrary for BaseDirectory {
  318. fn arbitrary(g: &mut Gen) -> Self {
  319. if bool::arbitrary(g) {
  320. BaseDirectory::App
  321. } else {
  322. BaseDirectory::Resource
  323. }
  324. }
  325. }
  326. impl Arbitrary for FileOperationOptions {
  327. fn arbitrary(g: &mut Gen) -> Self {
  328. Self {
  329. dir: Option::arbitrary(g),
  330. }
  331. }
  332. }
  333. impl Arbitrary for DirOperationOptions {
  334. fn arbitrary(g: &mut Gen) -> Self {
  335. Self {
  336. recursive: bool::arbitrary(g),
  337. dir: Option::arbitrary(g),
  338. }
  339. }
  340. }
  341. #[tauri_macros::module_command_test(fs_read_file, "fs > readFile")]
  342. #[quickcheck_macros::quickcheck]
  343. fn read_file(path: SafePathBuf, options: Option<FileOperationOptions>) {
  344. let res = super::Cmd::read_text_file(crate::test::mock_invoke_context(), path, options);
  345. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  346. }
  347. #[tauri_macros::module_command_test(fs_write_file, "fs > writeFile")]
  348. #[quickcheck_macros::quickcheck]
  349. fn write_file(path: SafePathBuf, contents: Vec<u8>, options: Option<FileOperationOptions>) {
  350. let res = super::Cmd::write_file(crate::test::mock_invoke_context(), path, contents, options);
  351. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  352. }
  353. #[tauri_macros::module_command_test(fs_read_dir, "fs > readDir")]
  354. #[quickcheck_macros::quickcheck]
  355. fn read_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
  356. let res = super::Cmd::read_dir(crate::test::mock_invoke_context(), path, options);
  357. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  358. }
  359. #[tauri_macros::module_command_test(fs_copy_file, "fs > copyFile")]
  360. #[quickcheck_macros::quickcheck]
  361. fn copy_file(
  362. source: SafePathBuf,
  363. destination: SafePathBuf,
  364. options: Option<FileOperationOptions>,
  365. ) {
  366. let res = super::Cmd::copy_file(
  367. crate::test::mock_invoke_context(),
  368. source,
  369. destination,
  370. options,
  371. );
  372. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  373. }
  374. #[tauri_macros::module_command_test(fs_create_dir, "fs > createDir")]
  375. #[quickcheck_macros::quickcheck]
  376. fn create_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
  377. let res = super::Cmd::create_dir(crate::test::mock_invoke_context(), path, options);
  378. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  379. }
  380. #[tauri_macros::module_command_test(fs_remove_dir, "fs > removeDir")]
  381. #[quickcheck_macros::quickcheck]
  382. fn remove_dir(path: SafePathBuf, options: Option<DirOperationOptions>) {
  383. let res = super::Cmd::remove_dir(crate::test::mock_invoke_context(), path, options);
  384. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  385. }
  386. #[tauri_macros::module_command_test(fs_remove_file, "fs > removeFile")]
  387. #[quickcheck_macros::quickcheck]
  388. fn remove_file(path: SafePathBuf, options: Option<FileOperationOptions>) {
  389. let res = super::Cmd::remove_file(crate::test::mock_invoke_context(), path, options);
  390. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  391. }
  392. #[tauri_macros::module_command_test(fs_rename_file, "fs > renameFile")]
  393. #[quickcheck_macros::quickcheck]
  394. fn rename_file(
  395. old_path: SafePathBuf,
  396. new_path: SafePathBuf,
  397. options: Option<FileOperationOptions>,
  398. ) {
  399. let res = super::Cmd::rename_file(
  400. crate::test::mock_invoke_context(),
  401. old_path,
  402. new_path,
  403. options,
  404. );
  405. assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
  406. }
  407. }