1use crate::config::Config;
6use crate::platform::Target;
7use json_patch::merge;
8use serde::de::DeserializeOwned;
9use serde_json::Value;
10use std::ffi::OsStr;
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14pub const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
16
17pub const SUPPORTED_FORMATS: &[ConfigFormat] =
19 &[ConfigFormat::Json, ConfigFormat::Json5, ConfigFormat::Toml];
20
21pub const ENABLED_FORMATS: &[ConfigFormat] = &[
23 ConfigFormat::Json,
24 #[cfg(feature = "config-json5")]
25 ConfigFormat::Json5,
26 #[cfg(feature = "config-toml")]
27 ConfigFormat::Toml,
28];
29
30#[derive(Debug, Copy, Clone)]
32pub enum ConfigFormat {
33 Json,
35 Json5,
37 Toml,
39}
40
41impl ConfigFormat {
42 pub fn into_file_name(self) -> &'static str {
44 match self {
45 Self::Json => "tauri.conf.json",
46 Self::Json5 => "tauri.conf.json5",
47 Self::Toml => "Tauri.toml",
48 }
49 }
50
51 fn into_platform_file_name(self, target: Target) -> &'static str {
52 match self {
53 Self::Json => match target {
54 Target::MacOS => "tauri.macos.conf.json",
55 Target::Windows => "tauri.windows.conf.json",
56 Target::Linux => "tauri.linux.conf.json",
57 Target::Android => "tauri.android.conf.json",
58 Target::Ios => "tauri.ios.conf.json",
59 },
60 Self::Json5 => match target {
61 Target::MacOS => "tauri.macos.conf.json5",
62 Target::Windows => "tauri.windows.conf.json5",
63 Target::Linux => "tauri.linux.conf.json5",
64 Target::Android => "tauri.android.conf.json5",
65 Target::Ios => "tauri.ios.conf.json5",
66 },
67 Self::Toml => match target {
68 Target::MacOS => "Tauri.macos.toml",
69 Target::Windows => "Tauri.windows.toml",
70 Target::Linux => "Tauri.linux.toml",
71 Target::Android => "Tauri.android.toml",
72 Target::Ios => "Tauri.ios.toml",
73 },
74 }
75 }
76}
77
78#[derive(Debug, Error)]
80#[non_exhaustive]
81pub enum ConfigError {
82 #[error("unable to parse JSON Tauri config file at {path} because {error}")]
84 FormatJson {
85 path: PathBuf,
87
88 error: serde_json::Error,
90 },
91
92 #[cfg(feature = "config-json5")]
94 #[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
95 FormatJson5 {
96 path: PathBuf,
98
99 error: ::json5::Error,
101 },
102
103 #[cfg(feature = "config-toml")]
105 #[error("unable to parse toml Tauri config file at {path} because {error}")]
106 FormatToml {
107 path: PathBuf,
109
110 error: Box<::toml::de::Error>,
112 },
113
114 #[error("unsupported format encountered {0}")]
116 UnsupportedFormat(String),
117
118 #[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
120 DisabledFormat {
121 extension: String,
123
124 feature: String,
126 },
127
128 #[error("unable to read Tauri config file at {path} because {error}")]
130 Io {
131 path: PathBuf,
133
134 error: std::io::Error,
136 },
137}
138
139pub fn folder_has_configuration_file(target: Target, folder: &Path) -> bool {
141 folder.join(ConfigFormat::Json.into_file_name()).exists()
142 || folder.join(ConfigFormat::Json5.into_file_name()).exists()
143 || folder.join(ConfigFormat::Toml.into_file_name()).exists()
144 || folder.join(ConfigFormat::Json.into_platform_file_name(target)).exists()
146 || folder.join(ConfigFormat::Json5.into_platform_file_name(target)).exists()
147 || folder.join(ConfigFormat::Toml.into_platform_file_name(target)).exists()
148}
149
150pub fn is_configuration_file(target: Target, path: &Path) -> bool {
152 path
153 .file_name()
154 .map(|file_name| {
155 file_name == OsStr::new(ConfigFormat::Json.into_file_name())
156 || file_name == OsStr::new(ConfigFormat::Json5.into_file_name())
157 || file_name == OsStr::new(ConfigFormat::Toml.into_file_name())
158 || file_name == OsStr::new(ConfigFormat::Json.into_platform_file_name(target))
160 || file_name == OsStr::new(ConfigFormat::Json5.into_platform_file_name(target))
161 || file_name == OsStr::new(ConfigFormat::Toml.into_platform_file_name(target))
162 })
163 .unwrap_or_default()
164}
165
166pub fn read_from(
181 target: Target,
182 root_dir: PathBuf,
183) -> Result<(Value, Option<PathBuf>), ConfigError> {
184 let mut config: Value = parse_value(target, root_dir.join("tauri.conf.json"))?.0;
185 if let Some((platform_config, path)) = read_platform(target, root_dir)? {
186 merge(&mut config, &platform_config);
187 Ok((config, Some(path)))
188 } else {
189 Ok((config, None))
190 }
191}
192
193pub fn read_platform(
197 target: Target,
198 root_dir: PathBuf,
199) -> Result<Option<(Value, PathBuf)>, ConfigError> {
200 let platform_config_path = root_dir.join(ConfigFormat::Json.into_platform_file_name(target));
201 if does_supported_file_name_exist(target, &platform_config_path) {
202 let (platform_config, path): (Value, PathBuf) = parse_value(target, platform_config_path)?;
203 Ok(Some((platform_config, path)))
204 } else {
205 Ok(None)
206 }
207}
208
209pub fn does_supported_file_name_exist(target: Target, path: impl Into<PathBuf>) -> bool {
214 let path = path.into();
215 let source_file_name = path.file_name().unwrap().to_str().unwrap();
216 let lookup_platform_config = ENABLED_FORMATS
217 .iter()
218 .any(|format| source_file_name == format.into_platform_file_name(target));
219 ENABLED_FORMATS.iter().any(|format| {
220 path
221 .with_file_name(if lookup_platform_config {
222 format.into_platform_file_name(target)
223 } else {
224 format.into_file_name()
225 })
226 .exists()
227 })
228}
229
230pub fn parse(target: Target, path: impl Into<PathBuf>) -> Result<(Config, PathBuf), ConfigError> {
245 do_parse(target, path.into())
246}
247
248pub fn parse_value(
250 target: Target,
251 path: impl Into<PathBuf>,
252) -> Result<(Value, PathBuf), ConfigError> {
253 do_parse(target, path.into())
254}
255
256fn do_parse<D: DeserializeOwned>(
257 target: Target,
258 path: PathBuf,
259) -> Result<(D, PathBuf), ConfigError> {
260 let file_name = path
261 .file_name()
262 .map(OsStr::to_string_lossy)
263 .unwrap_or_default();
264 let lookup_platform_config = ENABLED_FORMATS
265 .iter()
266 .any(|format| file_name == format.into_platform_file_name(target));
267
268 let json5 = path.with_file_name(if lookup_platform_config {
269 ConfigFormat::Json5.into_platform_file_name(target)
270 } else {
271 ConfigFormat::Json5.into_file_name()
272 });
273 let toml = path.with_file_name(if lookup_platform_config {
274 ConfigFormat::Toml.into_platform_file_name(target)
275 } else {
276 ConfigFormat::Toml.into_file_name()
277 });
278
279 let path_ext = path
280 .extension()
281 .map(OsStr::to_string_lossy)
282 .unwrap_or_default();
283
284 if path.exists() {
285 let raw = read_to_string(&path)?;
286
287 #[allow(clippy::let_and_return)]
289 let json = do_parse_json(&raw, &path);
290
291 #[cfg(feature = "config-json5")]
296 let json = {
297 match do_parse_json5(&raw, &path) {
298 json5 @ Ok(_) => json5,
299
300 Err(_) => json,
302 }
303 };
304
305 json.map(|j| (j, path))
306 } else if json5.exists() {
307 #[cfg(feature = "config-json5")]
308 {
309 let raw = read_to_string(&json5)?;
310 do_parse_json5(&raw, &json5).map(|config| (config, json5))
311 }
312
313 #[cfg(not(feature = "config-json5"))]
314 Err(ConfigError::DisabledFormat {
315 extension: ".json5".into(),
316 feature: "config-json5".into(),
317 })
318 } else if toml.exists() {
319 #[cfg(feature = "config-toml")]
320 {
321 let raw = read_to_string(&toml)?;
322 do_parse_toml(&raw, &toml).map(|config| (config, toml))
323 }
324
325 #[cfg(not(feature = "config-toml"))]
326 Err(ConfigError::DisabledFormat {
327 extension: ".toml".into(),
328 feature: "config-toml".into(),
329 })
330 } else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
331 Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
332 } else {
333 Err(ConfigError::Io {
334 path,
335 error: std::io::ErrorKind::NotFound.into(),
336 })
337 }
338}
339
340pub fn parse_json(raw: &str, path: &Path) -> Result<Config, ConfigError> {
344 do_parse_json(raw, path)
345}
346
347pub fn parse_json_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
351 do_parse_json(raw, path)
352}
353
354fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
355 serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
356 path: path.into(),
357 error,
358 })
359}
360
361#[cfg(feature = "config-json5")]
366pub fn parse_json5(raw: &str, path: &Path) -> Result<Config, ConfigError> {
367 do_parse_json5(raw, path)
368}
369
370#[cfg(feature = "config-json5")]
375pub fn parse_json5_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
376 do_parse_json5(raw, path)
377}
378
379#[cfg(feature = "config-json5")]
380fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
381 ::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
382 path: path.into(),
383 error,
384 })
385}
386
387#[cfg(feature = "config-toml")]
388fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
389 ::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
390 path: path.into(),
391 error: Box::new(error),
392 })
393}
394
395fn read_to_string(path: &Path) -> Result<String, ConfigError> {
397 std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
398 path: path.into(),
399 error,
400 })
401}