fontconfig_parser/types/
document.rs

1use crate::parser::parse_config;
2use crate::*;
3
4use std::collections::HashSet;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Clone, Debug, PartialEq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum ConfigPart {
11    Description(String),
12    SelectFont(SelectFont),
13    Dir(Dir),
14    CacheDir(CacheDir),
15    Include(Include),
16    Match(Match),
17    Config(Config),
18    Alias(Alias),
19    RemapDir(RemapDir),
20    ResetDirs,
21}
22
23#[derive(Clone, Debug, Default, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct FontConfig {
26    pub select_fonts: Vec<SelectFont>,
27    pub dirs: Vec<DirData>,
28    pub cache_dirs: Vec<PathBuf>,
29    pub remap_dirs: Vec<RemapDirData>,
30    pub matches: Vec<Match>,
31    pub config: Config,
32    pub aliases: Vec<Alias>,
33    pub config_files: HashSet<PathBuf>,
34}
35
36impl FontConfig {
37    pub fn merge_config<P: AsRef<Path> + ?Sized>(&mut self, config_path: &P) -> Result<()> {
38        match std::fs::canonicalize(&config_path) {
39            Ok(p) => {
40                if !self.config_files.insert(std::path::PathBuf::from(p)) {
41                    return Ok(());
42                }
43            }
44            Err(err) => return Err(Error::IoError(err)),
45        }
46
47        let config = fs::read_to_string(config_path.as_ref())?;
48        let xml_doc = roxmltree::Document::parse_with_options(
49            &config,
50            roxmltree::ParsingOptions {
51                allow_dtd: true,
52                ..Default::default()
53            },
54        )?;
55
56        for part in parse_config(&xml_doc)? {
57            match part? {
58                ConfigPart::Alias(alias) => self.aliases.push(alias),
59                ConfigPart::Config(mut c) => {
60                    self.config.rescans.append(&mut c.rescans);
61                    self.config.blanks.append(&mut c.blanks);
62                }
63                ConfigPart::Description(_) => {}
64                ConfigPart::Dir(dir) => self.dirs.push(DirData {
65                    path: dir.calculate_path(config_path),
66                    salt: dir.salt,
67                }),
68                ConfigPart::CacheDir(dir) => self.cache_dirs.push(dir.calculate_path(config_path)),
69                ConfigPart::Match(m) => self.matches.push(m),
70                ConfigPart::ResetDirs => self.dirs.clear(),
71                ConfigPart::SelectFont(s) => self.select_fonts.push(s),
72                ConfigPart::RemapDir(remap) => self.remap_dirs.push(RemapDirData {
73                    path: remap.calculate_path(config_path),
74                    salt: remap.salt,
75                    as_path: remap.as_path,
76                }),
77                ConfigPart::Include(dir) => {
78                    let include_path = dir.calculate_path(config_path);
79
80                    match self.include(&include_path) {
81                        Ok(_) => {}
82                        #[allow(unused_variables)]
83                        Err(err) => {
84                            if !dir.ignore_missing {
85                                #[cfg(feature = "log")]
86                                log::warn!("Failed to include {}: {}", include_path.display(), err);
87                            }
88                        }
89                    }
90                }
91            }
92        }
93
94        Ok(())
95    }
96
97    fn include(&mut self, include_path: &Path) -> Result<()> {
98        let meta = fs::metadata(include_path)?;
99        let ty = meta.file_type();
100
101        // fs::metadata follow symlink so ty is never symlink
102        if ty.is_file() {
103            self.merge_config(include_path)?;
104        } else if ty.is_dir() {
105            let dir = std::fs::read_dir(include_path)?;
106            let mut config_paths: Vec<_> = dir
107                .filter_map(|entry| {
108                    let entry = entry.ok()?;
109                    let ty = entry.file_type().ok()?;
110
111                    if ty.is_file() || ty.is_symlink() {
112                        Some(entry.path())
113                    } else {
114                        None
115                    }
116                })
117                .collect();
118
119            // Configs MUST be sorted in lexicographic order,
120            // otherwise `ConfigPart::ResetDirs` can occur out of intended order.
121            // See https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#:~:text=sorted%20in%20lexicographic%20order
122            config_paths.sort();
123
124            for config_path in config_paths {
125                match self.merge_config(&config_path) {
126                    Ok(_) => {}
127                    #[allow(unused_variables)]
128                    Err(err) => {
129                        #[cfg(feature = "log")]
130                        log::warn!("Failed to merge {}: {}", config_path.display(), err);
131                    }
132                }
133            }
134        }
135
136        Ok(())
137    }
138}
139
140macro_rules! define_config_part_from {
141	($($f:ident,)+) => {
142        $(
143            impl From<$f> for ConfigPart {
144                fn from(v: $f) -> Self {
145                    ConfigPart::$f(v)
146                }
147            }
148        )+
149	};
150}
151
152define_config_part_from! {
153    SelectFont,
154    Dir,
155    CacheDir,
156    Include,
157    Match,
158    Config,
159    Alias,
160    RemapDir,
161}
162
163#[derive(Clone, Debug, Default, PartialEq, Eq)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
165/// Final dir data
166pub struct DirData {
167    /// dir path
168    pub path: PathBuf,
169    /// 'salt' property affects to determine cache filename. this is useful for example when having different fonts sets on same path at container and share fonts from host on different font path.
170    pub salt: String,
171}
172
173#[derive(Clone, Debug, Default, PartialEq, Eq)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
175/// Final remap-dirs data
176pub struct RemapDirData {
177    /// dir path will be mapped as the path [`as-path`](Self::as_path) in cached information. This is useful if the directory name is an alias (via a bind mount or symlink) to another directory in the system for which cached font information is likely to exist.
178    pub path: PathBuf,
179    /// 'salt' property affects to determine cache filename. this is useful for example when having different fonts sets on same path at container and share fonts from host on different font path.
180    pub salt: String,
181    // remapped path
182    pub as_path: String,
183}