shuyi_assets/
import_settings.rs1use crate::prelude::*;
21
22use core::fmt;
23
24use ::ron::error::SpannedError;
25use ::ron::value::RawValue;
26use serde::Deserialize;
27use serde::de::{DeserializeOwned, Deserializer, MapAccess, Visitor};
28
29use crate::bundle::Bundle;
30use crate::error::{ContentError, codes};
31use crate::format::join;
32
33pub trait ImportSettings: DeserializeOwned + Default + 'static {
64 const SECTION: &'static str;
66}
67
68pub const SETTINGS_EXTENSION: &str = "meta";
70
71pub const FOLDER_SETTINGS: &str = "_folder.meta";
74
75pub fn is_settings_file(path: &str) -> bool {
78 let name = path.rsplit('/').next().unwrap_or(path);
79 name.rsplit_once('.').is_some_and(|(stem, extension)| {
80 !stem.is_empty() && extension.eq_ignore_ascii_case(SETTINGS_EXTENSION)
81 })
82}
83
84pub fn settings_of(file: &str) -> String {
87 format!("{file}.{SETTINGS_EXTENSION}")
88}
89
90pub fn settings_chain(file: &str) -> Vec<String> {
94 let folders = file.match_indices('/').rev().map(|(at, _)| &file[..at]);
96 let mut chain = vec![settings_of(file)];
97 chain.extend(folders.map(|dir| join(dir, FOLDER_SETTINGS)));
98 chain.push(FOLDER_SETTINGS.to_owned());
99 chain
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104struct Place {
105 line: u32,
106 column: u32,
107}
108
109impl Place {
110 fn of(source: &str, offset: usize) -> Self {
112 let before = source.get(..offset).unwrap_or(source);
113 let line = 1 + before.matches('\n').count();
114 let column = 1 + before.rsplit('\n').next().unwrap_or("").chars().count();
115 Self {
116 line: to_u32(line),
117 column: to_u32(column),
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
125pub(crate) struct Section<'a> {
126 pub(crate) name: String,
128 text: &'a str,
130 start: Place,
132 at: Place,
135}
136
137impl Section<'_> {
138 pub(crate) fn error(&self, shown: &str, message: impl Into<String>) -> ContentError {
140 ContentError::new(shown, message).at(self.at.line, Some(self.at.column))
141 }
142
143 fn moved(&self, shown: &str, e: &SpannedError) -> ContentError {
146 let pos = e.span.end;
148 let line = to_u32(pos.line.saturating_sub(1));
149 let column = to_u32(pos.col);
150 let (line, column) = if line == 0 {
151 (
152 self.start.line,
153 self.start.column.saturating_add(column.saturating_sub(1)),
154 )
155 } else {
156 (self.start.line.saturating_add(line), column)
157 };
158 ContentError::new(shown, e.code.to_string())
159 .at(line, Some(column))
160 .with_code(codes::PARSE)
161 }
162}
163
164struct RawSections<'a>(Vec<(String, &'a RawValue)>);
167
168impl<'de> Deserialize<'de> for RawSections<'de> {
169 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
170 deserializer.deserialize_any(SectionsVisitor)
171 }
172}
173
174struct SectionsVisitor;
176
177impl<'de> Visitor<'de> for SectionsVisitor {
178 type Value = RawSections<'de>;
179
180 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 f.write_str("a struct of sections, as `(onebit: (dither: bayer))`")
182 }
183
184 fn visit_unit<E>(self) -> Result<Self::Value, E> {
185 Ok(RawSections(Vec::new()))
186 }
187
188 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
189 let mut sections = Vec::new();
190 while let Some(name) = map.next_key::<String>()? {
191 sections.push((name, map.next_value::<&'de RawValue>()?));
192 }
193 Ok(RawSections(sections))
194 }
195}
196
197pub(crate) fn sections<'a>(
202 shown: &str,
203 bytes: &'a [u8],
204 max: usize,
205) -> Result<Vec<Section<'a>>, Vec<ContentError>> {
206 if bytes.len() > max {
207 let message = format!(
208 "the file is {} bytes; a settings file is read up to {max}",
209 bytes.len()
210 );
211 return Err(vec![
212 ContentError::new(shown, message).with_code(codes::TOO_BIG),
213 ]);
214 }
215 let source = core::str::from_utf8(bytes).map_err(|_| {
216 vec![ContentError::new(shown, "file is not valid UTF-8").with_code(codes::NOT_TEXT)]
217 })?;
218 let raw: RawSections<'a> =
219 ::ron::from_str(source).map_err(|e| vec![crate::ron::error(shown, &e)])?;
220 let base = source.as_ptr().addr();
221 let sections: Vec<Section<'a>> = raw
222 .0
223 .into_iter()
224 .map(|(name, value)| {
225 let text = value.get_ron();
226 let offset = text
228 .as_ptr()
229 .addr()
230 .checked_sub(base)
231 .filter(|&offset| offset <= source.len())
232 .unwrap_or(0);
233 let at = name_offset(source, offset, &name)
234 .unwrap_or(offset + text.len() - text.trim_start().len());
235 Section {
236 name,
237 text,
238 start: Place::of(source, offset),
239 at: Place::of(source, at),
240 }
241 })
242 .collect();
243 let twice: Vec<ContentError> = sections
244 .iter()
245 .enumerate()
246 .filter(|&(i, section)| sections[..i].iter().any(|s| s.name == section.name))
247 .map(|(_, section)| {
248 let message = format!(
249 "the section \"{}\" is given twice; a settings file gives each section once",
250 section.name
251 );
252 section.error(shown, message).with_code(codes::DUPLICATE)
253 })
254 .collect();
255 if twice.is_empty() {
256 Ok(sections)
257 } else {
258 Err(twice)
259 }
260}
261
262fn name_offset(source: &str, value: usize, name: &str) -> Option<usize> {
265 let before = source.get(..value)?.strip_suffix(':')?.trim_end();
266 before.ends_with(name).then(|| before.len() - name.len())
267}
268
269pub(crate) fn read_section<S: ImportSettings>(
271 shown: &str,
272 section: &Section<'_>,
273) -> Result<S, ContentError> {
274 ::ron::from_str(section.text).map_err(|e| section.moved(shown, &e))
275}
276
277pub(crate) type CheckSection = fn(shown: &str, section: &Section<'_>) -> Option<ContentError>;
280
281pub(crate) fn check_section<S: ImportSettings>(
283 shown: &str,
284 section: &Section<'_>,
285) -> Option<ContentError> {
286 read_section::<S>(shown, section).err()
287}
288
289pub(crate) fn orphan(bundle: &Bundle, path: &str, shown: &str) -> Option<ContentError> {
293 if path.rsplit('/').next() == Some(FOLDER_SETTINGS) {
294 return None;
295 }
296 let file = path.get(..path.len().checked_sub(SETTINGS_EXTENSION.len() + 1)?)?;
297 let file_shown = bundle.display_path(file);
298 if is_settings_file(file) {
299 let message = format!(
300 "these are settings of the settings file {file_shown}; settings belong to an asset, \
301 and a folder's defaults go in {FOLDER_SETTINGS}"
302 );
303 return Some(ContentError::new(shown, message));
304 }
305 if bundle.has(file) || bundle.left_out(file) {
306 return None;
307 }
308 let message = format!(
309 "these are the settings of {file_shown}, which the bundle doesn't have; rename them \
310 with the file, or remove them"
311 );
312 Some(ContentError::new(shown, message).with_code(codes::MISSING_FILE))
313}
314
315fn to_u32(n: usize) -> u32 {
316 u32::try_from(n).unwrap_or(u32::MAX)
317}
318
319#[cfg(test)]
320mod tests;