parse.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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::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. /// Reads the configuration from the given root directory.
  126. ///
  127. /// It first looks for a `tauri.conf.json[5]` file on the given directory. The file must exist.
  128. /// Then it looks for a platform-specific configuration file:
  129. /// - `tauri.macos.conf.json[5]` on macOS
  130. /// - `tauri.linux.conf.json[5]` on Linux
  131. /// - `tauri.windows.conf.json[5]` on Windows
  132. /// Merging the configurations using [JSON Merge Patch (RFC 7396)].
  133. ///
  134. /// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
  135. pub fn read_from(root_dir: PathBuf) -> Result<Value, ConfigError> {
  136. let mut config: Value = parse_value(root_dir.join("tauri.conf.json"))?.0;
  137. if let Some((platform_config, _)) = read_platform(root_dir)? {
  138. merge(&mut config, &platform_config);
  139. }
  140. Ok(config)
  141. }
  142. /// Reads the platform-specific configuration file from the given root directory if it exists.
  143. ///
  144. /// Check [`read_from`] for more information.
  145. pub fn read_platform(root_dir: PathBuf) -> Result<Option<(Value, PathBuf)>, ConfigError> {
  146. let platform_config_path = root_dir.join(ConfigFormat::Json.into_platform_file_name());
  147. if does_supported_file_name_exist(&platform_config_path) {
  148. let (platform_config, path): (Value, PathBuf) = parse_value(platform_config_path)?;
  149. Ok(Some((platform_config, path)))
  150. } else {
  151. Ok(None)
  152. }
  153. }
  154. /// Check if a supported config file exists at path.
  155. ///
  156. /// The passed path is expected to be the path to the "default" configuration format, in this case
  157. /// JSON with `.json`.
  158. pub fn does_supported_file_name_exist(path: impl Into<PathBuf>) -> bool {
  159. let path = path.into();
  160. let source_file_name = path.file_name().unwrap().to_str().unwrap();
  161. let lookup_platform_config = ENABLED_FORMATS
  162. .iter()
  163. .any(|format| source_file_name == format.into_platform_file_name());
  164. ENABLED_FORMATS.iter().any(|format| {
  165. path
  166. .with_file_name(if lookup_platform_config {
  167. format.into_platform_file_name()
  168. } else {
  169. format.into_file_name()
  170. })
  171. .exists()
  172. })
  173. }
  174. /// Parse the config from path, including alternative formats.
  175. ///
  176. /// Hierarchy:
  177. /// 1. Check if `tauri.conf.json` exists
  178. /// a. Parse it with `serde_json`
  179. /// b. Parse it with `json5` if `serde_json` fails
  180. /// c. Return original `serde_json` error if all above steps failed
  181. /// 2. Check if `tauri.conf.json5` exists
  182. /// a. Parse it with `json5`
  183. /// b. Return error if all above steps failed
  184. /// 3. Check if `Tauri.json` exists
  185. /// a. Parse it with `toml`
  186. /// b. Return error if all above steps failed
  187. /// 4. Return error if all above steps failed
  188. pub fn parse(path: impl Into<PathBuf>) -> Result<(Config, PathBuf), ConfigError> {
  189. do_parse(path.into())
  190. }
  191. /// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
  192. pub fn parse_value(path: impl Into<PathBuf>) -> Result<(Value, PathBuf), ConfigError> {
  193. do_parse(path.into())
  194. }
  195. fn do_parse<D: DeserializeOwned>(path: PathBuf) -> Result<(D, PathBuf), ConfigError> {
  196. let file_name = path
  197. .file_name()
  198. .map(OsStr::to_string_lossy)
  199. .unwrap_or_default();
  200. let lookup_platform_config = ENABLED_FORMATS
  201. .iter()
  202. .any(|format| file_name == format.into_platform_file_name());
  203. let json5 = path.with_file_name(if lookup_platform_config {
  204. ConfigFormat::Json5.into_platform_file_name()
  205. } else {
  206. ConfigFormat::Json5.into_file_name()
  207. });
  208. let toml = path.with_file_name(if lookup_platform_config {
  209. ConfigFormat::Toml.into_platform_file_name()
  210. } else {
  211. ConfigFormat::Toml.into_file_name()
  212. });
  213. let path_ext = path
  214. .extension()
  215. .map(OsStr::to_string_lossy)
  216. .unwrap_or_default();
  217. if path.exists() {
  218. let raw = read_to_string(&path)?;
  219. // to allow us to easily use the compile-time #[cfg], we always bind
  220. #[allow(clippy::let_and_return)]
  221. let json = do_parse_json(&raw, &path);
  222. // we also want to support **valid** json5 in the .json extension if the feature is enabled.
  223. // if the json5 is not valid the serde_json error for regular json will be returned.
  224. // this could be a bit confusing, so we may want to encourage users using json5 to use the
  225. // .json5 extension instead of .json
  226. #[cfg(feature = "config-json5")]
  227. let json = {
  228. match do_parse_json5(&raw, &path) {
  229. json5 @ Ok(_) => json5,
  230. // assume any errors from json5 in a .json file is because it's not json5
  231. Err(_) => json,
  232. }
  233. };
  234. json.map(|j| (j, path))
  235. } else if json5.exists() {
  236. #[cfg(feature = "config-json5")]
  237. {
  238. let raw = read_to_string(&json5)?;
  239. do_parse_json5(&raw, &path).map(|config| (config, json5))
  240. }
  241. #[cfg(not(feature = "config-json5"))]
  242. Err(ConfigError::DisabledFormat {
  243. extension: ".json5".into(),
  244. feature: "config-json5".into(),
  245. })
  246. } else if toml.exists() {
  247. #[cfg(feature = "config-toml")]
  248. {
  249. let raw = read_to_string(&toml)?;
  250. do_parse_toml(&raw, &path).map(|config| (config, toml))
  251. }
  252. #[cfg(not(feature = "config-toml"))]
  253. Err(ConfigError::DisabledFormat {
  254. extension: ".toml".into(),
  255. feature: "config-toml".into(),
  256. })
  257. } else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
  258. Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
  259. } else {
  260. Err(ConfigError::Io {
  261. path,
  262. error: std::io::ErrorKind::NotFound.into(),
  263. })
  264. }
  265. }
  266. /// "Low-level" helper to parse JSON into a [`Config`].
  267. ///
  268. /// `raw` should be the contents of the file that is represented by `path`.
  269. pub fn parse_json(raw: &str, path: &Path) -> Result<Config, ConfigError> {
  270. do_parse_json(raw, path)
  271. }
  272. /// "Low-level" helper to parse JSON into a JSON [`Value`].
  273. ///
  274. /// `raw` should be the contents of the file that is represented by `path`.
  275. pub fn parse_json_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
  276. do_parse_json(raw, path)
  277. }
  278. fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
  279. serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
  280. path: path.into(),
  281. error,
  282. })
  283. }
  284. /// "Low-level" helper to parse JSON5 into a [`Config`].
  285. ///
  286. /// `raw` should be the contents of the file that is represented by `path`. This function requires
  287. /// the `config-json5` feature to be enabled.
  288. #[cfg(feature = "config-json5")]
  289. pub fn parse_json5(raw: &str, path: &Path) -> Result<Config, ConfigError> {
  290. do_parse_json5(raw, path)
  291. }
  292. /// "Low-level" helper to parse JSON5 into a JSON [`Value`].
  293. ///
  294. /// `raw` should be the contents of the file that is represented by `path`. This function requires
  295. /// the `config-json5` feature to be enabled.
  296. #[cfg(feature = "config-json5")]
  297. pub fn parse_json5_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
  298. do_parse_json5(raw, path)
  299. }
  300. #[cfg(feature = "config-json5")]
  301. fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
  302. ::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
  303. path: path.into(),
  304. error,
  305. })
  306. }
  307. #[cfg(feature = "config-toml")]
  308. fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
  309. ::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
  310. path: path.into(),
  311. error,
  312. })
  313. }
  314. /// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
  315. fn read_to_string(path: &Path) -> Result<String, ConfigError> {
  316. std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
  317. path: path.into(),
  318. error,
  319. })
  320. }