parse.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use crate::config::Config;
  5. use json_patch::merge;
  6. use serde::de::DeserializeOwned;
  7. use serde_json::Value;
  8. use std::ffi::OsStr;
  9. use std::path::{Path, PathBuf};
  10. use thiserror::Error;
  11. /// All extensions that are possibly supported, but perhaps not enabled.
  12. pub const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
  13. /// All configuration formats that are possibly supported, but perhaps not enabled.
  14. pub const SUPPORTED_FORMATS: &[ConfigFormat] =
  15. &[ConfigFormat::Json, ConfigFormat::Json5, ConfigFormat::Toml];
  16. /// All configuration formats that are currently enabled.
  17. pub const ENABLED_FORMATS: &[ConfigFormat] = &[
  18. ConfigFormat::Json,
  19. #[cfg(feature = "config-json5")]
  20. ConfigFormat::Json5,
  21. #[cfg(feature = "config-toml")]
  22. ConfigFormat::Toml,
  23. ];
  24. /// The available configuration formats.
  25. #[derive(Debug, Copy, Clone)]
  26. pub enum ConfigFormat {
  27. /// The default JSON (tauri.conf.json) format.
  28. Json,
  29. /// The JSON5 (tauri.conf.json5) format.
  30. Json5,
  31. /// The TOML (Tauri.toml file) format.
  32. Toml,
  33. }
  34. impl ConfigFormat {
  35. /// Maps the config format to its file name.
  36. pub fn into_file_name(self) -> &'static str {
  37. match self {
  38. Self::Json => "tauri.conf.json",
  39. Self::Json5 => "tauri.conf.json5",
  40. Self::Toml => "Tauri.toml",
  41. }
  42. }
  43. fn into_platform_file_name(self) -> &'static str {
  44. match self {
  45. Self::Json => {
  46. if cfg!(target_os = "macos") {
  47. "tauri.macos.conf.json"
  48. } else if cfg!(windows) {
  49. "tauri.windows.conf.json"
  50. } else {
  51. "tauri.linux.conf.json"
  52. }
  53. }
  54. Self::Json5 => {
  55. if cfg!(target_os = "macos") {
  56. "tauri.macos.conf.json5"
  57. } else if cfg!(windows) {
  58. "tauri.windows.conf.json5"
  59. } else {
  60. "tauri.linux.conf.json5"
  61. }
  62. }
  63. Self::Toml => {
  64. if cfg!(target_os = "macos") {
  65. "Tauri.macos.toml"
  66. } else if cfg!(windows) {
  67. "Tauri.windows.toml"
  68. } else {
  69. "Tauri.linux.toml"
  70. }
  71. }
  72. }
  73. }
  74. }
  75. /// Represents all the errors that can happen while reading the config.
  76. #[derive(Debug, Error)]
  77. #[non_exhaustive]
  78. pub enum ConfigError {
  79. /// Failed to parse from JSON.
  80. #[error("unable to parse JSON Tauri config file at {path} because {error}")]
  81. FormatJson {
  82. /// The path that failed to parse into JSON.
  83. path: PathBuf,
  84. /// The parsing [`serde_json::Error`].
  85. error: serde_json::Error,
  86. },
  87. /// Failed to parse from JSON5.
  88. #[cfg(feature = "config-json5")]
  89. #[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
  90. FormatJson5 {
  91. /// The path that failed to parse into JSON5.
  92. path: PathBuf,
  93. /// The parsing [`json5::Error`].
  94. error: ::json5::Error,
  95. },
  96. /// Failed to parse from TOML.
  97. #[cfg(feature = "config-toml")]
  98. #[error("unable to parse toml Tauri config file at {path} because {error}")]
  99. FormatToml {
  100. /// The path that failed to parse into TOML.
  101. path: PathBuf,
  102. /// The parsing [`toml::Error`].
  103. error: ::toml::de::Error,
  104. },
  105. /// Unknown config file name encountered.
  106. #[error("unsupported format encountered {0}")]
  107. UnsupportedFormat(String),
  108. /// Known file extension encountered, but corresponding parser is not enabled (cargo features).
  109. #[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
  110. DisabledFormat {
  111. /// The extension encountered.
  112. extension: String,
  113. /// The cargo feature to enable it.
  114. feature: String,
  115. },
  116. /// A generic IO error with context of what caused it.
  117. #[error("unable to read Tauri config file at {path} because {error}")]
  118. Io {
  119. /// The path the IO error occurred on.
  120. path: PathBuf,
  121. /// The [`std::io::Error`].
  122. error: std::io::Error,
  123. },
  124. }
  125. /// Determines if the given folder has a configuration file.
  126. pub fn folder_has_configuration_file(folder: &Path) -> bool {
  127. folder.join(ConfigFormat::Json.into_file_name()).exists()
  128. || folder.join(ConfigFormat::Json5.into_file_name()).exists()
  129. || folder.join(ConfigFormat::Toml.into_file_name()).exists()
  130. // platform file names
  131. || folder.join(ConfigFormat::Json.into_platform_file_name()).exists()
  132. || folder.join(ConfigFormat::Json5.into_platform_file_name()).exists()
  133. || folder.join(ConfigFormat::Toml.into_platform_file_name()).exists()
  134. }
  135. /// Determines if the given file path represents a Tauri configuration file.
  136. pub fn is_configuration_file(path: &Path) -> bool {
  137. path
  138. .file_name()
  139. .map(|file_name| {
  140. file_name == OsStr::new(ConfigFormat::Json.into_file_name())
  141. || file_name == OsStr::new(ConfigFormat::Json5.into_file_name())
  142. || file_name == OsStr::new(ConfigFormat::Toml.into_file_name())
  143. // platform file names
  144. || file_name == OsStr::new(ConfigFormat::Json.into_platform_file_name())
  145. || file_name == OsStr::new(ConfigFormat::Json5.into_platform_file_name())
  146. || file_name == OsStr::new(ConfigFormat::Toml.into_platform_file_name())
  147. })
  148. .unwrap_or_default()
  149. }
  150. /// Reads the configuration from the given root directory.
  151. ///
  152. /// It first looks for a `tauri.conf.json[5]` file on the given directory. The file must exist.
  153. /// Then it looks for a platform-specific configuration file:
  154. /// - `tauri.macos.conf.json[5]` on macOS
  155. /// - `tauri.linux.conf.json[5]` on Linux
  156. /// - `tauri.windows.conf.json[5]` on Windows
  157. /// Merging the configurations using [JSON Merge Patch (RFC 7396)].
  158. ///
  159. /// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
  160. pub fn read_from(root_dir: PathBuf) -> Result<Value, ConfigError> {
  161. let mut config: Value = parse_value(root_dir.join("tauri.conf.json"))?.0;
  162. if let Some((platform_config, _)) = read_platform(root_dir)? {
  163. merge(&mut config, &platform_config);
  164. }
  165. Ok(config)
  166. }
  167. /// Reads the platform-specific configuration file from the given root directory if it exists.
  168. ///
  169. /// Check [`read_from`] for more information.
  170. pub fn read_platform(root_dir: PathBuf) -> Result<Option<(Value, PathBuf)>, ConfigError> {
  171. let platform_config_path = root_dir.join(ConfigFormat::Json.into_platform_file_name());
  172. if does_supported_file_name_exist(&platform_config_path) {
  173. let (platform_config, path): (Value, PathBuf) = parse_value(platform_config_path)?;
  174. Ok(Some((platform_config, path)))
  175. } else {
  176. Ok(None)
  177. }
  178. }
  179. /// Check if a supported config file exists at path.
  180. ///
  181. /// The passed path is expected to be the path to the "default" configuration format, in this case
  182. /// JSON with `.json`.
  183. pub fn does_supported_file_name_exist(path: impl Into<PathBuf>) -> bool {
  184. let path = path.into();
  185. let source_file_name = path.file_name().unwrap().to_str().unwrap();
  186. let lookup_platform_config = ENABLED_FORMATS
  187. .iter()
  188. .any(|format| source_file_name == format.into_platform_file_name());
  189. ENABLED_FORMATS.iter().any(|format| {
  190. path
  191. .with_file_name(if lookup_platform_config {
  192. format.into_platform_file_name()
  193. } else {
  194. format.into_file_name()
  195. })
  196. .exists()
  197. })
  198. }
  199. /// Parse the config from path, including alternative formats.
  200. ///
  201. /// Hierarchy:
  202. /// 1. Check if `tauri.conf.json` exists
  203. /// a. Parse it with `serde_json`
  204. /// b. Parse it with `json5` if `serde_json` fails
  205. /// c. Return original `serde_json` error if all above steps failed
  206. /// 2. Check if `tauri.conf.json5` exists
  207. /// a. Parse it with `json5`
  208. /// b. Return error if all above steps failed
  209. /// 3. Check if `Tauri.json` exists
  210. /// a. Parse it with `toml`
  211. /// b. Return error if all above steps failed
  212. /// 4. Return error if all above steps failed
  213. pub fn parse(path: impl Into<PathBuf>) -> Result<(Config, PathBuf), ConfigError> {
  214. do_parse(path.into())
  215. }
  216. /// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
  217. pub fn parse_value(path: impl Into<PathBuf>) -> Result<(Value, PathBuf), ConfigError> {
  218. do_parse(path.into())
  219. }
  220. fn do_parse<D: DeserializeOwned>(path: PathBuf) -> Result<(D, PathBuf), ConfigError> {
  221. let file_name = path
  222. .file_name()
  223. .map(OsStr::to_string_lossy)
  224. .unwrap_or_default();
  225. let lookup_platform_config = ENABLED_FORMATS
  226. .iter()
  227. .any(|format| file_name == format.into_platform_file_name());
  228. let json5 = path.with_file_name(if lookup_platform_config {
  229. ConfigFormat::Json5.into_platform_file_name()
  230. } else {
  231. ConfigFormat::Json5.into_file_name()
  232. });
  233. let toml = path.with_file_name(if lookup_platform_config {
  234. ConfigFormat::Toml.into_platform_file_name()
  235. } else {
  236. ConfigFormat::Toml.into_file_name()
  237. });
  238. let path_ext = path
  239. .extension()
  240. .map(OsStr::to_string_lossy)
  241. .unwrap_or_default();
  242. if path.exists() {
  243. let raw = read_to_string(&path)?;
  244. // to allow us to easily use the compile-time #[cfg], we always bind
  245. #[allow(clippy::let_and_return)]
  246. let json = do_parse_json(&raw, &path);
  247. // we also want to support **valid** json5 in the .json extension if the feature is enabled.
  248. // if the json5 is not valid the serde_json error for regular json will be returned.
  249. // this could be a bit confusing, so we may want to encourage users using json5 to use the
  250. // .json5 extension instead of .json
  251. #[cfg(feature = "config-json5")]
  252. let json = {
  253. match do_parse_json5(&raw, &path) {
  254. json5 @ Ok(_) => json5,
  255. // assume any errors from json5 in a .json file is because it's not json5
  256. Err(_) => json,
  257. }
  258. };
  259. json.map(|j| (j, path))
  260. } else if json5.exists() {
  261. #[cfg(feature = "config-json5")]
  262. {
  263. let raw = read_to_string(&json5)?;
  264. do_parse_json5(&raw, &path).map(|config| (config, json5))
  265. }
  266. #[cfg(not(feature = "config-json5"))]
  267. Err(ConfigError::DisabledFormat {
  268. extension: ".json5".into(),
  269. feature: "config-json5".into(),
  270. })
  271. } else if toml.exists() {
  272. #[cfg(feature = "config-toml")]
  273. {
  274. let raw = read_to_string(&toml)?;
  275. do_parse_toml(&raw, &path).map(|config| (config, toml))
  276. }
  277. #[cfg(not(feature = "config-toml"))]
  278. Err(ConfigError::DisabledFormat {
  279. extension: ".toml".into(),
  280. feature: "config-toml".into(),
  281. })
  282. } else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
  283. Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
  284. } else {
  285. Err(ConfigError::Io {
  286. path,
  287. error: std::io::ErrorKind::NotFound.into(),
  288. })
  289. }
  290. }
  291. /// "Low-level" helper to parse JSON into a [`Config`].
  292. ///
  293. /// `raw` should be the contents of the file that is represented by `path`.
  294. pub fn parse_json(raw: &str, path: &Path) -> Result<Config, ConfigError> {
  295. do_parse_json(raw, path)
  296. }
  297. /// "Low-level" helper to parse JSON into a JSON [`Value`].
  298. ///
  299. /// `raw` should be the contents of the file that is represented by `path`.
  300. pub fn parse_json_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
  301. do_parse_json(raw, path)
  302. }
  303. fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
  304. serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
  305. path: path.into(),
  306. error,
  307. })
  308. }
  309. /// "Low-level" helper to parse JSON5 into a [`Config`].
  310. ///
  311. /// `raw` should be the contents of the file that is represented by `path`. This function requires
  312. /// the `config-json5` feature to be enabled.
  313. #[cfg(feature = "config-json5")]
  314. pub fn parse_json5(raw: &str, path: &Path) -> Result<Config, ConfigError> {
  315. do_parse_json5(raw, path)
  316. }
  317. /// "Low-level" helper to parse JSON5 into a JSON [`Value`].
  318. ///
  319. /// `raw` should be the contents of the file that is represented by `path`. This function requires
  320. /// the `config-json5` feature to be enabled.
  321. #[cfg(feature = "config-json5")]
  322. pub fn parse_json5_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
  323. do_parse_json5(raw, path)
  324. }
  325. #[cfg(feature = "config-json5")]
  326. fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
  327. ::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
  328. path: path.into(),
  329. error,
  330. })
  331. }
  332. #[cfg(feature = "config-toml")]
  333. fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
  334. ::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
  335. path: path.into(),
  336. error,
  337. })
  338. }
  339. /// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
  340. fn read_to_string(path: &Path) -> Result<String, ConfigError> {
  341. std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
  342. path: path.into(),
  343. error,
  344. })
  345. }