tauri_utils/config_v1/
parse.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7use std::ffi::OsStr;
8use std::path::{Path, PathBuf};
9use thiserror::Error;
10
11/// All extensions that are possibly supported, but perhaps not enabled.
12const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
13
14/// All configuration formats that are currently enabled.
15const ENABLED_FORMATS: &[ConfigFormat] = &[
16  ConfigFormat::Json,
17  #[cfg(feature = "config-json5")]
18  ConfigFormat::Json5,
19  #[cfg(feature = "config-toml")]
20  ConfigFormat::Toml,
21];
22
23/// The available configuration formats.
24#[derive(Debug, Copy, Clone)]
25enum ConfigFormat {
26  /// The default JSON (tauri.conf.json) format.
27  Json,
28  /// The JSON5 (tauri.conf.json5) format.
29  Json5,
30  /// The TOML (Tauri.toml file) format.
31  Toml,
32}
33
34impl ConfigFormat {
35  /// Maps the config format to its file name.
36  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
44  fn into_platform_file_name(self) -> &'static str {
45    match self {
46      Self::Json => {
47        if cfg!(target_os = "macos") {
48          "tauri.macos.conf.json"
49        } else if cfg!(windows) {
50          "tauri.windows.conf.json"
51        } else {
52          "tauri.linux.conf.json"
53        }
54      }
55      Self::Json5 => {
56        if cfg!(target_os = "macos") {
57          "tauri.macos.conf.json5"
58        } else if cfg!(windows) {
59          "tauri.windows.conf.json5"
60        } else {
61          "tauri.linux.conf.json5"
62        }
63      }
64      Self::Toml => {
65        if cfg!(target_os = "macos") {
66          "Tauri.macos.toml"
67        } else if cfg!(windows) {
68          "Tauri.windows.toml"
69        } else {
70          "Tauri.linux.toml"
71        }
72      }
73    }
74  }
75}
76
77/// Represents all the errors that can happen while reading the config.
78#[derive(Debug, Error)]
79#[non_exhaustive]
80pub enum ConfigError {
81  /// Failed to parse from JSON.
82  #[error("unable to parse JSON Tauri config file at {path} because {error}")]
83  FormatJson {
84    /// The path that failed to parse into JSON.
85    path: PathBuf,
86
87    /// The parsing [`serde_json::Error`].
88    error: serde_json::Error,
89  },
90
91  /// Failed to parse from JSON5.
92  #[cfg(feature = "config-json5")]
93  #[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
94  FormatJson5 {
95    /// The path that failed to parse into JSON5.
96    path: PathBuf,
97
98    /// The parsing [`json5::Error`].
99    error: ::json5::Error,
100  },
101
102  /// Failed to parse from TOML.
103  #[cfg(feature = "config-toml")]
104  #[error("unable to parse toml Tauri config file at {path} because {error}")]
105  FormatToml {
106    /// The path that failed to parse into TOML.
107    path: PathBuf,
108
109    /// The parsing [`toml::Error`].
110    error: ::toml::de::Error,
111  },
112
113  /// Unknown config file name encountered.
114  #[error("unsupported format encountered {0}")]
115  UnsupportedFormat(String),
116
117  /// Known file extension encountered, but corresponding parser is not enabled (cargo features).
118  #[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
119  DisabledFormat {
120    /// The extension encountered.
121    extension: String,
122
123    /// The cargo feature to enable it.
124    feature: String,
125  },
126
127  /// A generic IO error with context of what caused it.
128  #[error("unable to read Tauri config file at {path} because {error}")]
129  Io {
130    /// The path the IO error occurred on.
131    path: PathBuf,
132
133    /// The [`std::io::Error`].
134    error: std::io::Error,
135  },
136}
137
138/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
139pub fn parse_value(path: impl Into<PathBuf>) -> Result<(Value, PathBuf), ConfigError> {
140  do_parse(path.into())
141}
142
143fn do_parse<D: DeserializeOwned>(path: PathBuf) -> Result<(D, PathBuf), ConfigError> {
144  let file_name = path
145    .file_name()
146    .map(OsStr::to_string_lossy)
147    .unwrap_or_default();
148  let lookup_platform_config = ENABLED_FORMATS
149    .iter()
150    .any(|format| file_name == format.into_platform_file_name());
151
152  let json5 = path.with_file_name(if lookup_platform_config {
153    ConfigFormat::Json5.into_platform_file_name()
154  } else {
155    ConfigFormat::Json5.into_file_name()
156  });
157  let toml = path.with_file_name(if lookup_platform_config {
158    ConfigFormat::Toml.into_platform_file_name()
159  } else {
160    ConfigFormat::Toml.into_file_name()
161  });
162
163  let path_ext = path
164    .extension()
165    .map(OsStr::to_string_lossy)
166    .unwrap_or_default();
167
168  if path.exists() {
169    let raw = read_to_string(&path)?;
170
171    // to allow us to easily use the compile-time #[cfg], we always bind
172    #[allow(clippy::let_and_return)]
173    let json = do_parse_json(&raw, &path);
174
175    // we also want to support **valid** json5 in the .json extension if the feature is enabled.
176    // if the json5 is not valid the serde_json error for regular json will be returned.
177    // this could be a bit confusing, so we may want to encourage users using json5 to use the
178    // .json5 extension instead of .json
179    #[cfg(feature = "config-json5")]
180    let json = {
181      match do_parse_json5(&raw, &path) {
182        json5 @ Ok(_) => json5,
183
184        // assume any errors from json5 in a .json file is because it's not json5
185        Err(_) => json,
186      }
187    };
188
189    json.map(|j| (j, path))
190  } else if json5.exists() {
191    #[cfg(feature = "config-json5")]
192    {
193      let raw = read_to_string(&json5)?;
194      do_parse_json5(&raw, &path).map(|config| (config, json5))
195    }
196
197    #[cfg(not(feature = "config-json5"))]
198    Err(ConfigError::DisabledFormat {
199      extension: ".json5".into(),
200      feature: "config-json5".into(),
201    })
202  } else if toml.exists() {
203    #[cfg(feature = "config-toml")]
204    {
205      let raw = read_to_string(&toml)?;
206      do_parse_toml(&raw, &path).map(|config| (config, toml))
207    }
208
209    #[cfg(not(feature = "config-toml"))]
210    Err(ConfigError::DisabledFormat {
211      extension: ".toml".into(),
212      feature: "config-toml".into(),
213    })
214  } else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
215    Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
216  } else {
217    Err(ConfigError::Io {
218      path,
219      error: std::io::ErrorKind::NotFound.into(),
220    })
221  }
222}
223
224fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
225  serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
226    path: path.into(),
227    error,
228  })
229}
230
231#[cfg(feature = "config-json5")]
232fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
233  ::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
234    path: path.into(),
235    error,
236  })
237}
238
239#[cfg(feature = "config-toml")]
240fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
241  ::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
242    path: path.into(),
243    error,
244  })
245}
246
247/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
248fn read_to_string(path: &Path) -> Result<String, ConfigError> {
249  std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
250    path: path.into(),
251    error,
252  })
253}