Skip to main content

shuyi_assets/
import_settings.rs

1//! Import settings (ADR-0057): how one file, or every file of a folder, is
2//! read or baked. A file's own settings are beside it, its full name kept
3//! (`images/hero.png.meta`); a folder's defaults are in its
4//! `_folder.meta`, for every file in it and below it. A settings file is
5//! RON, a struct of sections, each read by the step it is named after:
6//! `(onebit: (dither: bayer), indexed: (group: "ui"))`.
7//!
8//! A section is a type that implements [`ImportSettings`], registered in a
9//! [`Formats`](crate::Formats) at start-up
10//! ([`register_import_settings`](crate::Formats::register_import_settings)).
11//! A format reads its section with
12//! [`DecodeCx::import_settings`](crate::DecodeCx::import_settings): from
13//! the nearest file of the chain that has it ([`settings_chain`]), whole,
14//! or its `Default` when none does.
15//! [`check_import_settings`](crate::Formats::check_import_settings) checks
16//! every settings file of a bundle against the registered sections.
17//!
18//! No format may claim `meta`, so the walkers over a folder never read a
19//! settings file as an asset.
20use 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
33/// A section of a settings file: a struct the step named [`SECTION`]
34/// reads (`onebit`, `indexed`), never named after a file format.
35///
36/// A section is a struct with `#[serde(default, deny_unknown_fields)]`:
37/// every field may be left out, and a typo is an error.
38/// [`register_import_settings`](crate::Formats::register_import_settings)
39/// refuses one without them. Its fields hold integers, bools, enums and
40/// strings, never floats (ADR-0057 ยง9).
41///
42/// ```
43/// use serde::Deserialize;
44/// use shuyi_assets::import_settings::ImportSettings;
45///
46/// /// How a picture is flipped.
47/// #[derive(Debug, Default, PartialEq, Eq, Deserialize)]
48/// #[serde(default, deny_unknown_fields)]
49/// struct Mirror {
50///     /// Whether it is flipped left to right.
51///     mirror: bool,
52/// }
53///
54/// impl ImportSettings for Mirror {
55///     const SECTION: &'static str = "mirror";
56/// }
57///
58/// let mut formats = shuyi_assets::Formats::standard();
59/// assert!(formats.register_import_settings::<Mirror>().is_ok());
60/// ```
61///
62/// [`SECTION`]: Self::SECTION
63pub trait ImportSettings: DeserializeOwned + Default + 'static {
64    /// The section's name in a settings file: the step that reads it.
65    const SECTION: &'static str;
66}
67
68/// The extension of a settings file, which no format may claim.
69pub const SETTINGS_EXTENSION: &str = "meta";
70
71/// The name of a folder's settings file: the defaults of every file in
72/// the folder and below it.
73pub const FOLDER_SETTINGS: &str = "_folder.meta";
74
75/// Whether the file at `path` is a settings file: its extension is
76/// [`SETTINGS_EXTENSION`] (in any case).
77pub 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
84/// The path of the settings file of the file at `file`: `images/hero.png`
85/// has `images/hero.png.meta`.
86pub fn settings_of(file: &str) -> String {
87    format!("{file}.{SETTINGS_EXTENSION}")
88}
89
90/// The settings files the file at `file` reads a section from, nearest
91/// first: its own ([`settings_of`]), then the [`FOLDER_SETTINGS`] of its
92/// folder and of each folder above it, up to the root.
93pub fn settings_chain(file: &str) -> Vec<String> {
94    // Each folder is the path up to one of its slashes, the nearest last.
95    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/// A place in a settings file: a 1-based line and column (in characters).
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104struct Place {
105    line: u32,
106    column: u32,
107}
108
109impl Place {
110    /// The place of byte `offset` of `source`.
111    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/// One section of a settings file: its name, its text (from right after
123/// the colon, with the whitespace before the value) and where it is.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub(crate) struct Section<'a> {
126    /// Its name.
127    pub(crate) name: String,
128    /// Its value's text.
129    text: &'a str,
130    /// Where the text starts: RON counts an error in it from here.
131    start: Place,
132    /// Where its name is (where its value starts if the name isn't found
133    /// before the colon): where errors about the whole section point.
134    at: Place,
135}
136
137impl Section<'_> {
138    /// An error about the whole section, of the file shown as `shown`.
139    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    /// RON's error `e` inside the section's text, moved to its place in the
144    /// file shown as `shown`.
145    fn moved(&self, shown: &str, e: &SpannedError) -> ContentError {
146        // `span.end` is the offending token, as `ron::parse_ron` reports it.
147        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
164/// A settings file's sections as RON gives them, each value's text
165/// borrowed from the source.
166struct 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
174/// Reads a struct of sections, or `()` for none.
175struct 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
197/// The sections of the settings file shown as `shown`, whose bytes are
198/// `bytes`, read at most `max` bytes: each with its text and place. Every
199/// problem of the file as a whole: too big, not UTF-8, bad RON, a section
200/// given twice.
201pub(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            // The text is borrowed from `source`, so it starts inside it.
227            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
262/// The offset in `source` of the section name `name`, which ends before
263/// the colon that ends right before byte `value` (spaces between).
264fn 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
269/// `section` of the settings file shown as `shown`, read as `S`.
270pub(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
277/// What a registry keeps of a section: whether a section's text reads as
278/// its type, and the error if it doesn't.
279pub(crate) type CheckSection = fn(shown: &str, section: &Section<'_>) -> Option<ContentError>;
280
281/// The [`CheckSection`] of `S`.
282pub(crate) fn check_section<S: ImportSettings>(
283    shown: &str,
284    section: &Section<'_>,
285) -> Option<ContentError> {
286    read_section::<S>(shown, section).err()
287}
288
289/// The error for the settings file at `path` (shown as `shown`) if it
290/// belongs to no file: a `<name>.meta` whose file `bundle` lacks and
291/// doesn't leave out on purpose, or whose file is itself a settings file.
292pub(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;