1use crate::Error;
2use std::ffi::OsStr;
3use std::fs::File;
4use std::io::prelude::*;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
8pub(crate) enum FileType {
9 Properties,
10 Hocon,
11 Json,
12 All,
13}
14
15#[derive(Default, Debug)]
16pub(crate) struct FileRead {
17 pub(crate) properties: Option<String>,
18 pub(crate) json: Option<String>,
19 pub(crate) hocon: Option<String>,
20}
21impl FileRead {
22 fn from_file_type(ft: &FileType, s: String) -> Self {
23 match ft {
24 FileType::Properties => Self {
25 properties: Some(s),
26 ..Default::default()
27 },
28 FileType::Json => Self {
29 json: Some(s),
30 ..Default::default()
31 },
32 FileType::Hocon => Self {
33 hocon: Some(s),
34 ..Default::default()
35 },
36 FileType::All => unimplemented!(),
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
42pub(crate) struct ConfFileMeta {
43 path: PathBuf,
44 full_path: PathBuf,
45 file_type: FileType,
46}
47impl ConfFileMeta {
48 pub(crate) fn from_path(path: PathBuf) -> Self {
49 let file = path
50 .file_name()
51 .expect("got a path without a filename")
52 .to_str()
53 .expect("got invalid UTF-8 path");
54 let mut parent_path = path.clone();
55 parent_path.pop();
56
57 Self {
58 path: parent_path,
59 full_path: path.clone(),
60 file_type: match Path::new(file).extension().and_then(OsStr::to_str) {
61 Some("properties") => FileType::Properties,
62 Some("json") => FileType::Json,
63 Some("conf") => FileType::Hocon,
64 _ => FileType::All,
65 },
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
71pub(crate) struct HoconLoaderConfig {
72 pub(crate) include_depth: u8,
73 pub(crate) file_meta: Option<ConfFileMeta>,
74 pub(crate) system: bool,
75 #[cfg(feature = "url-support")]
76 pub(crate) external_url: bool,
77 pub(crate) strict: bool,
78 pub(crate) max_include_depth: u8,
79}
80
81impl Default for HoconLoaderConfig {
82 fn default() -> Self {
83 Self {
84 include_depth: 0,
85 file_meta: None,
86 system: true,
87 #[cfg(feature = "url-support")]
88 external_url: true,
89 strict: false,
90 max_include_depth: 10,
91 }
92 }
93}
94
95impl HoconLoaderConfig {
96 pub(crate) fn included_from(&self) -> Self {
97 Self {
98 include_depth: self.include_depth + 1,
99 ..self.clone()
100 }
101 }
102
103 pub(crate) fn with_file(&self, path: PathBuf) -> Self {
104 match self.file_meta.as_ref() {
105 Some(file_meta) => Self {
106 file_meta: Some(ConfFileMeta::from_path(file_meta.clone().path.join(path))),
107 ..self.clone()
108 },
109 None => Self {
110 file_meta: Some(ConfFileMeta::from_path(path)),
111 ..self.clone()
112 },
113 }
114 }
115
116 pub(crate) fn parse_str_to_internal(
117 &self,
118 s: FileRead,
119 ) -> Result<crate::internals::HoconInternal, Error> {
120 let mut internal = crate::internals::HoconInternal::empty();
121 if let Some(properties) = s.properties {
122 internal = internal.add(
123 java_properties::read(properties.as_bytes())
124 .map(crate::internals::HoconInternal::from_properties)
125 .map_err(|_| crate::Error::Parse)?,
126 );
127 };
128 if let Some(json) = s.json {
129 internal = internal.add(
130 crate::parser::root(format!("{}\n\0", json.replace('\r', "\n")).as_bytes(), self)
131 .map_err(|_| crate::Error::Parse)
132 .and_then(|(remaining, parsed)| {
133 if Self::remaining_only_whitespace(remaining) {
134 parsed
135 } else if self.strict {
136 Err(crate::Error::Deserialization {
137 message: String::from("file could not be parsed completely"),
138 })
139 } else {
140 parsed
141 }
142 })?,
143 );
144 };
145 if let Some(hocon) = s.hocon {
146 internal = internal.add(
147 crate::parser::root(
148 format!("{}\n\0", hocon.replace('\r', "\n")).as_bytes(),
149 self,
150 )
151 .map_err(|_| crate::Error::Parse)
152 .and_then(|(remaining, parsed)| {
153 if Self::remaining_only_whitespace(remaining) {
154 parsed
155 } else if self.strict {
156 Err(crate::Error::Deserialization {
157 message: String::from("file could not be parsed completely"),
158 })
159 } else {
160 parsed
161 }
162 })?,
163 );
164 };
165
166 Ok(internal)
167 }
168
169 fn remaining_only_whitespace(remaining: &[u8]) -> bool {
170 remaining
171 .iter()
172 .find(|c| {
173 **c != 10 && **c != 13 && **c != 0
176 })
177 .map(|_| false)
178 .unwrap_or(true)
179 }
180
181 pub(crate) fn read_file_to_string(path: PathBuf) -> Result<String, Error> {
182 let mut file = File::open(path.as_os_str())?;
183 let mut contents = String::new();
184 file.read_to_string(&mut contents)?;
185 Ok(contents)
186 }
187
188 pub(crate) fn read_file(&self) -> Result<FileRead, Error> {
189 let full_path = self
190 .file_meta
191 .clone()
192 .expect("missing file metadata")
193 .full_path;
194 match self.file_meta.as_ref().map(|fm| &fm.file_type) {
195 Some(FileType::All) => Ok(FileRead {
196 hocon: Self::read_file_to_string({
197 let mut path = full_path.clone();
198 if !path.exists() {
199 path.set_extension("conf");
200 }
201 path
202 })
203 .ok(),
204 json: Self::read_file_to_string({
205 let mut path = full_path.clone();
206 path.set_extension("json");
207 path
208 })
209 .ok(),
210 properties: Self::read_file_to_string({
211 let mut path = full_path;
212 path.set_extension("properties");
213 path
214 })
215 .ok(),
216 }),
217 Some(ft) => Ok(FileRead::from_file_type(
218 ft,
219 Self::read_file_to_string(full_path)?,
220 )),
221 _ => unimplemented!(),
222 }
223 }
224
225 #[cfg(feature = "url-support")]
226 pub(crate) fn load_url(&self, url: &str) -> Result<crate::internals::HoconInternal, Error> {
227 if let Ok(parsed_url) = reqwest::Url::parse(url) {
228 if parsed_url.scheme() == "file" {
229 if let Ok(path) = parsed_url.to_file_path() {
230 let include_config = self.included_from().with_file(path);
231 let s = include_config.read_file()?;
232 Ok(include_config.parse_str_to_internal(s).map_err(|_| {
233 crate::Error::Include {
234 path: String::from(url),
235 }
236 })?)
237 } else {
238 Err(crate::Error::Include {
239 path: String::from(url),
240 })
241 }
242 } else if self.external_url {
243 let body = reqwest::blocking::get(parsed_url)
244 .and_then(reqwest::blocking::Response::text)
245 .map_err(|_| crate::Error::Include {
246 path: String::from(url),
247 })?;
248
249 Ok(self.parse_str_to_internal(FileRead {
250 hocon: Some(body),
251 ..Default::default()
252 })?)
253 } else {
254 Err(crate::Error::Include {
255 path: String::from(url),
256 })
257 }
258 } else {
259 Err(crate::Error::Include {
260 path: String::from(url),
261 })
262 }
263 }
264}