Skip to main content

webfont_generator/cache/
mod.rs

1//! Opt-in persistent generation cache (`cacheLocation`).
2//!
3//! One file per font identity holds:
4//! - level 2: parsed geometry and processed glyphs keyed by SVG content hash, so unchanged icons
5//!   skip parsing and path processing on a cold run;
6//! - level 1: finished font bytes per format, keyed by everything that affects those bytes, so
7//!   unchanged formats skip glyph work and encoding (including WOFF2 compression) entirely.
8//!
9//! Variant families use their own file (the design list is part of its identity), with processed
10//! glyphs keyed by content hash and advance width instead of by source.
11//!
12//! Every read or write failure degrades to a miss: the cache can make generation faster, never
13//! fail it or change its output.
14
15mod codec;
16#[cfg(all(test, feature = "fs"))]
17mod tests;
18
19use std::array::from_fn;
20use std::collections::HashMap;
21#[cfg(feature = "fs")]
22use std::fs::{self, File};
23#[cfg(feature = "fs")]
24use std::io::{ErrorKind, Read, Result};
25use std::mem::take;
26#[cfg(feature = "fs")]
27use std::ops::Deref;
28use std::ops::Range;
29#[cfg(feature = "fs")]
30use std::path::{Path, PathBuf};
31#[cfg(feature = "fs")]
32use std::process;
33#[cfg(feature = "fs")]
34use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
35use std::sync::{Arc, OnceLock};
36
37use flate2::Crc;
38use md5::compute;
39use serde::Serialize;
40use serde_json::{Value, json, to_vec};
41
42use self::codec::{Reader, Writer};
43use crate::input::{
44    LoadedSvgSource, ResolvedGenerationOptions, ResolvedVariants, VariantFamilySources,
45    VariantGlyphSource,
46};
47use crate::rendering::font_settings_json;
48use crate::result::FontOutputs;
49use crate::svg::types::{CachedGlyph, CachedProcessedGlyph, GlyphCache, SvgOptions};
50use crate::svg::{VariantGlyphCache, source_content_hash};
51use crate::types::FontType;
52
53const MAGIC: &[u8; 8] = b"WFGCACHE";
54/// Bump when the file layout or any persisted type's meaning changes.
55const FORMAT_VERSION: u32 = 1;
56const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
57#[cfg(feature = "fs")]
58const EXTENSION: &str = "wfcache";
59/// Entries kept per format: the current one plus the most recent different one, so builds that
60/// differ only in a format option (e.g. dev and production WOFF2 quality) don't evict each other.
61const ENTRIES_PER_FORMAT: usize = 2;
62
63const FONT_TYPES: [FontType; 5] = [
64    FontType::Svg,
65    FontType::Ttf,
66    FontType::Eot,
67    FontType::Woff,
68    FontType::Woff2,
69];
70
71fn font_type_tag(font_type: FontType) -> u8 {
72    FONT_TYPES
73        .iter()
74        .position(|candidate| *candidate == font_type)
75        .expect("every font type is listed") as u8
76}
77
78/// One format's finished output, shared without copying with the result that uses it.
79#[derive(Clone)]
80pub(crate) enum CachedFont {
81    Svg(Arc<String>),
82    Ttf(Arc<Vec<u8>>),
83    Eot(Arc<Vec<u8>>),
84    Woff(Arc<Vec<u8>>),
85    Woff2(Arc<Vec<u8>>),
86}
87
88impl CachedFont {
89    pub(crate) fn font_type(&self) -> FontType {
90        match self {
91            Self::Svg(_) => FontType::Svg,
92            Self::Ttf(_) => FontType::Ttf,
93            Self::Eot(_) => FontType::Eot,
94            Self::Woff(_) => FontType::Woff,
95            Self::Woff2(_) => FontType::Woff2,
96        }
97    }
98
99    fn bytes(&self) -> &[u8] {
100        match self {
101            Self::Svg(text) => text.as_bytes(),
102            Self::Ttf(bytes) | Self::Eot(bytes) | Self::Woff(bytes) | Self::Woff2(bytes) => bytes,
103        }
104    }
105
106    fn from_outputs(outputs: &FontOutputs, font_type: FontType) -> Option<Self> {
107        Some(match font_type {
108            FontType::Svg => Self::Svg(outputs.svg_font.clone()?),
109            FontType::Ttf => Self::Ttf(outputs.ttf_font.clone()?),
110            FontType::Eot => Self::Eot(outputs.eot_font.clone()?),
111            FontType::Woff => Self::Woff(outputs.woff_font.clone()?),
112            FontType::Woff2 => Self::Woff2(outputs.woff2_font.clone()?),
113        })
114    }
115
116    fn decode(font_type: FontType, bytes: Vec<u8>) -> Option<Self> {
117        Some(match font_type {
118            FontType::Svg => Self::Svg(Arc::new(String::from_utf8(bytes).ok()?)),
119            FontType::Ttf => Self::Ttf(Arc::new(bytes)),
120            FontType::Eot => Self::Eot(Arc::new(bytes)),
121            FontType::Woff => Self::Woff(Arc::new(bytes)),
122            FontType::Woff2 => Self::Woff2(Arc::new(bytes)),
123        })
124    }
125
126    /// Put this format into a result's outputs.
127    pub(crate) fn place(self, outputs: &mut FontOutputs) {
128        match self {
129            Self::Svg(text) => outputs.svg_font = Some(text),
130            Self::Ttf(bytes) => outputs.ttf_font = Some(bytes),
131            Self::Eot(bytes) => outputs.eot_font = Some(bytes),
132            Self::Woff(bytes) => outputs.woff_font = Some(bytes),
133            Self::Woff2(bytes) => outputs.woff2_font = Some(bytes),
134        }
135    }
136}
137
138#[derive(Clone)]
139struct FontEntry {
140    key: [u8; 16],
141    /// Identifies the ordered glyph names, codepoints, and contents the bytes were built from.
142    glyph_set: [u8; 16],
143    font: CachedFont,
144}
145
146/// Counts reported by [`clear_cache`].
147#[cfg(feature = "fs")]
148#[derive(Default)]
149#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
150pub struct ClearCacheReport {
151    /// Number of cache files removed.
152    pub files_removed: usize,
153    /// Total size of the removed files in bytes.
154    pub bytes_removed: u64,
155}
156
157/// Remove every file the generation cache created in `cache_location`, including temporary
158/// files left by interrupted writes. Other files in the directory are left untouched, and a
159/// missing directory is not an error.
160#[cfg(feature = "fs")]
161pub fn clear_cache(cache_location: impl AsRef<Path>) -> Result<ClearCacheReport> {
162    let entries = match fs::read_dir(cache_location) {
163        Ok(entries) => entries,
164        Err(error) if error.kind() == ErrorKind::NotFound => return Ok(ClearCacheReport::default()),
165        Err(error) => return Err(error),
166    };
167    let mut report = ClearCacheReport::default();
168    for entry in entries {
169        let entry = entry?;
170        let path = entry.path();
171        if !entry.file_type()?.is_file() || !created_by_cache(&path) {
172            continue;
173        }
174        let size = entry.metadata().map(|metadata| metadata.len()).unwrap_or(0);
175        match fs::remove_file(&path) {
176            Ok(()) => {
177                report.files_removed += 1;
178                report.bytes_removed += size;
179            }
180            // A concurrent run may have renamed or removed it first.
181            Err(error) if error.kind() == ErrorKind::NotFound => {}
182            Err(error) => return Err(error),
183        }
184    }
185    Ok(report)
186}
187
188/// Level 2 entries: per-icon work keyed by source content hash.
189#[derive(Default)]
190pub(crate) struct Glyphs {
191    processed_signature: Option<[u8; 16]>,
192    parsed: HashMap<[u8; 16], Arc<CachedGlyph>>,
193    processed: HashMap<[u8; 16], CachedProcessedGlyph>,
194    /// Variant families: processed glyphs per content hash and advance width bits.
195    variant_processed: HashMap<[u8; 16], Vec<(u64, CachedProcessedGlyph)>>,
196}
197
198/// Cache contents for one generation, independent of where the bytes live: identity, decoded
199/// entries, and everything needed to look up and produce updated bytes.
200pub(crate) struct CacheState {
201    fingerprint: [u8; 16],
202    /// Section checksums (glyphs, fonts) and length of the bytes last loaded or returned by
203    /// `update`, to skip rewriting identical bytes without keeping a second copy of them.
204    loaded: Option<([u32; 2], usize)>,
205    /// The loaded bytes and the glyph section's range, kept until the glyphs are needed: a run
206    /// whose fonts all hit never checks or decodes them. Set and cleared together with `loaded`,
207    /// which holds the section's expected checksum.
208    pending: Option<(Vec<u8>, Range<usize>)>,
209    /// Level 2, decoded on first use; `None` inside when the loaded glyph section is corrupt.
210    glyphs: OnceLock<Option<Glyphs>>,
211    fonts: Vec<FontEntry>,
212    glyph_set: [u8; 16],
213    /// Digest of the settings each format depends on, in `FONT_TYPES` order, kept so keys can be
214    /// recomputed when the glyphs change.
215    #[cfg(feature = "in_memory")]
216    settings_digests: [[u8; 16]; 5],
217    /// Level 1 key per format, in `FONT_TYPES` order.
218    font_keys: [[u8; 16]; 5],
219}
220
221impl CacheState {
222    pub(crate) fn new(options: &ResolvedGenerationOptions, sources: &[LoadedSvgSource]) -> Self {
223        let settings = font_settings_json(options);
224        Self::with_identity(&settings, &settings, glyph_set(options, sources))
225    }
226
227    /// A variant family's cache: a family marker keeps its file apart from a single-design cache
228    /// for the same font name, and the designs (names, weights, default) key its fonts.
229    pub(crate) fn new_variant<Files: AsRef<[LoadedSvgSource]>>(
230        options: &ResolvedGenerationOptions,
231        variants: &ResolvedVariants,
232        family: &VariantFamilySources<Files>,
233    ) -> Self {
234        let settings = font_settings_json(options);
235        let with_variants = |value: Value| {
236            let mut settings = settings.clone();
237            let object = settings.as_object_mut().expect("settings are an object");
238            object.insert("variants".to_owned(), value);
239            settings
240        };
241        // Design names, weights and the default only shape the variable-font tables, so they key
242        // the fonts but not the file: renaming a design keeps every saved glyph. The marker alone
243        // keeps a family's file apart from a single design's.
244        let identity = with_variants(json!(true));
245        let designs: Vec<_> = variants
246            .variants
247            .iter()
248            .map(|variant| json!([variant.name, variant.weight]))
249            .collect();
250        let keys = with_variants(json!({ "default": variants.default_index, "designs": designs }));
251        Self::with_identity(&identity, &keys, variant_glyph_set(family))
252    }
253
254    /// `identity` decides which file (and so which saved glyphs) applies; `keys` adds what only
255    /// the finished fonts depend on.
256    fn with_identity(identity: &Value, keys: &Value, glyph_set: [u8; 16]) -> Self {
257        // Level 2 validity depends on everything except per-format encoding options and palette
258        // slots (only the color tables use them), which only level 1 keys need. Those stay out of
259        // the file identity so formats share one file and slot changes keep saved glyphs.
260        let mut glyph_settings = with_format_options(identity, &[]);
261        if let Some(settings) = glyph_settings.as_object_mut() {
262            settings.remove("colorSlots");
263        }
264        let fingerprint = digest(&(FORMAT_VERSION, ENGINE_VERSION, &glyph_settings));
265        let settings_digests = format_settings_digests(keys);
266        Self {
267            fingerprint,
268            loaded: None,
269            pending: None,
270            glyphs: OnceLock::new(),
271            fonts: Vec::new(),
272            glyph_set,
273            #[cfg(feature = "in_memory")]
274            settings_digests,
275            font_keys: font_keys(&settings_digests, &glyph_set),
276        }
277    }
278
279    #[cfg(feature = "in_memory")]
280    /// Re-identify the glyphs (from [`glyph_set`] or [`variant_glyph_set`]) after a membership
281    /// or content change that kept the settings.
282    pub(crate) fn refresh_glyph_set(&mut self, glyph_set: [u8; 16]) {
283        self.glyph_set = glyph_set;
284        self.font_keys = font_keys(&self.settings_digests, &self.glyph_set);
285    }
286
287    /// Adopt previously saved bytes. Anything that doesn't decode for this identity is ignored.
288    pub(crate) fn load(&mut self, bytes: Vec<u8>) {
289        if let Some((checksums, glyphs)) = self.decode(&bytes) {
290            self.loaded = Some((checksums, bytes.len()));
291            self.pending = Some((bytes, glyphs));
292        } else {
293            self.fonts.clear();
294        }
295    }
296
297    /// Saved per-icon work, checked and decoded on first use. `None` when the loaded glyph
298    /// section is corrupt: callers then treat the whole cache as a miss.
299    pub(crate) fn glyphs(&self) -> Option<&Glyphs> {
300        self.glyphs
301            .get_or_init(|| match &self.pending {
302                None => Some(Glyphs::default()),
303                Some((bytes, range)) => {
304                    let section = &bytes[range.clone()];
305                    let expected = self.loaded.map(|(checksums, _)| checksums[0]);
306                    (Some(checksum(section)) == expected)
307                        .then(|| decode_glyphs(section))
308                        .flatten()
309                }
310            })
311            .as_ref()
312    }
313
314    /// Replace level 2 with `glyphs`, dropping the loaded bytes. Unless the loaded glyph section
315    /// was checked and valid, its checksum mustn't let identical new bytes skip a write.
316    fn replace_glyphs(&mut self, glyphs: Glyphs) {
317        if !matches!(self.glyphs.get(), Some(Some(_))) {
318            self.loaded = None;
319        }
320        self.pending = None;
321        self.glyphs = OnceLock::from(Some(glyphs));
322    }
323
324    /// Free the decoded glyphs once they seeded a glyph cache: `update` and `update_variants`
325    /// rebuild them from it.
326    #[cfg(feature = "in_memory")]
327    pub(crate) fn release_glyphs(&mut self) {
328        self.replace_glyphs(Glyphs::default());
329    }
330
331    fn font_key(&self, font_type: FontType) -> [u8; 16] {
332        self.font_keys[usize::from(font_type_tag(font_type))]
333    }
334
335    /// Cached output for each requested format that has a current entry.
336    pub(crate) fn font_hits(&self, types: &[FontType]) -> Vec<CachedFont> {
337        types
338            .iter()
339            .filter_map(|font_type| {
340                let key = self.font_key(*font_type);
341                self.fonts
342                    .iter()
343                    .find(|entry| entry.font.font_type() == *font_type && entry.key == key)
344                    .map(|entry| entry.font.clone())
345            })
346            .collect()
347    }
348
349    /// Seed an in-memory glyph cache so unchanged glyphs skip parse and processing.
350    pub(crate) fn hydrate(
351        &self,
352        svg_options: &SvgOptions,
353        sources: &[LoadedSvgSource],
354    ) -> GlyphCache {
355        let mut cache = GlyphCache::default();
356        let Some(glyphs) = self.glyphs() else {
357            return cache;
358        };
359        for source in sources {
360            // Saved work only applies in the same color mode the glyph is selected for now.
361            let color = svg_options.selects_color(&source.glyph_name);
362            let hash = source_content_hash(&source.contents);
363            if let Some(glyph) = glyphs.parsed.get(&hash)
364                && glyph.color_layers.is_some() == color
365            {
366                cache.by_content_hash.insert(hash, glyph.clone());
367            }
368            if let Some(glyph) = glyphs.processed.get(&hash)
369                && glyph.color_layers.is_some() == color
370            {
371                cache
372                    .processed_entries
373                    .insert(source.id.clone(), glyph.clone());
374            }
375        }
376        cache.processed_signature = glyphs.processed_signature;
377        cache
378    }
379
380    /// Seed a variant glyph cache so unchanged glyphs skip parse and processing.
381    pub(crate) fn hydrate_variants<Files: AsRef<[LoadedSvgSource]>>(
382        &self,
383        svg_options: &SvgOptions,
384        variants: &[Files],
385    ) -> VariantGlyphCache {
386        let Some(glyphs) = self.glyphs() else {
387            return VariantGlyphCache::default();
388        };
389        let mut cache = VariantGlyphCache {
390            signature: glyphs.processed_signature,
391            ..Default::default()
392        };
393        for (index, files) in variants.iter().enumerate() {
394            let mut parsed = GlyphCache::default();
395            for source in files.as_ref() {
396                let color = svg_options.selects_color(&source.glyph_name);
397                let hash = source_content_hash(&source.contents);
398                let Some(glyph) = glyphs
399                    .parsed
400                    .get(&hash)
401                    .filter(|glyph| glyph.color_layers.is_some() == color)
402                else {
403                    continue;
404                };
405                // A known content hash marks the entry current, so preparation keeps it.
406                parsed.entries.insert(source.id.clone(), glyph.clone());
407                parsed.content_hashes.insert(source.id.clone(), hash);
408                parsed.by_content_hash.insert(hash, glyph.clone());
409                let processed = glyphs.variant_processed.get(&hash).into_iter().flatten();
410                for (advance, glyph) in
411                    processed.filter(|(_, glyph)| glyph.color_layers.is_some() == color)
412                {
413                    cache
414                        .processed
415                        .insert((index, source.id.clone(), *advance), glyph.clone());
416                }
417            }
418            cache.parsed.push(parsed);
419        }
420        cache
421    }
422
423    /// Replace level 2 with `glyph_cache` (current sources only) and add level 1 entries from
424    /// `outputs`. Returns the new bytes, or `None` when they equal the last loaded or returned
425    /// ones.
426    pub(crate) fn update(
427        &mut self,
428        sources: &[LoadedSvgSource],
429        glyph_cache: &GlyphCache,
430        outputs: &FontOutputs,
431        types: &[FontType],
432    ) -> Option<Vec<u8>> {
433        let mut glyphs = Glyphs::default();
434        // ponytail: one entry per content hash, so an SVG used as both a color and a monochrome
435        // glyph keeps only one mode's work (the other recomputes). Key by mode if that shows up.
436        for source in sources {
437            let hash = source_content_hash(&source.contents);
438            // Glyphs reused straight from the cache are only indexed by content hash.
439            if let Some(glyph) = glyph_cache
440                .entries
441                .get(&source.id)
442                .or_else(|| glyph_cache.by_content_hash.get(&hash))
443            {
444                glyphs.parsed.insert(hash, glyph.clone());
445            }
446            if let Some(glyph) = glyph_cache.processed_entries.get(&source.id) {
447                glyphs.processed.insert(hash, glyph.clone());
448            }
449        }
450        glyphs.processed_signature = glyph_cache.processed_signature;
451        self.replace_glyphs(glyphs);
452        self.finish(outputs, types)
453    }
454
455    /// Variant counterpart of [`Self::update`]: level 2 comes from `glyph_cache`, restricted to
456    /// the current `variants` sources.
457    pub(crate) fn update_variants<Files: AsRef<[LoadedSvgSource]>>(
458        &mut self,
459        variants: &[Files],
460        glyph_cache: &VariantGlyphCache,
461        outputs: &FontOutputs,
462        types: &[FontType],
463    ) -> Option<Vec<u8>> {
464        let mut glyphs = Glyphs::default();
465        let mut hashes = HashMap::new();
466        for (index, files) in variants.iter().enumerate() {
467            let Some(parsed) = glyph_cache.parsed.get(index) else {
468                continue;
469            };
470            for source in files.as_ref() {
471                let hash = source_content_hash(&source.contents);
472                if parsed.content_hashes.get(&source.id) != Some(&hash) {
473                    continue;
474                }
475                if let Some(glyph) = parsed.entries.get(&source.id) {
476                    glyphs.parsed.insert(hash, glyph.clone());
477                    hashes.insert((index, source.id.as_str()), hash);
478                }
479            }
480        }
481        // `processed` iterates in arbitrary order: keeping only glyphs in the same color mode as
482        // the saved parsed glyph makes every candidate identical, so the bytes are deterministic.
483        for ((index, id, advance), glyph) in &glyph_cache.processed {
484            if let Some(hash) = hashes.get(&(*index, id.as_str()))
485                && glyphs.parsed.get(hash).is_some_and(|parsed| {
486                    parsed.color_layers.is_some() == glyph.color_layers.is_some()
487                })
488            {
489                let entries = glyphs.variant_processed.entry(*hash).or_default();
490                if entries.iter().all(|(known, _)| known != advance) {
491                    entries.push((*advance, glyph.clone()));
492                }
493            }
494        }
495        glyphs.processed_signature = glyph_cache.signature;
496        self.replace_glyphs(glyphs);
497        self.finish(outputs, types)
498    }
499
500    /// Add level 1 entries from `outputs` and encode. Returns the new bytes, or `None` when they
501    /// equal the last loaded or returned ones.
502    fn finish(&mut self, outputs: &FontOutputs, types: &[FontType]) -> Option<Vec<u8>> {
503        let mut fonts: Vec<FontEntry> = types
504            .iter()
505            .filter_map(|font_type| {
506                Some(FontEntry {
507                    key: self.font_key(*font_type),
508                    glyph_set: self.glyph_set,
509                    font: CachedFont::from_outputs(outputs, *font_type)?,
510                })
511            })
512            .collect();
513        for entry in take(&mut self.fonts) {
514            let same_type = fonts
515                .iter()
516                .filter(|kept| kept.font.font_type() == entry.font.font_type())
517                .collect::<Vec<_>>();
518            // Older entries only survive for the same glyphs (an option-only difference); once the
519            // icons change they can never match again.
520            if entry.glyph_set == self.glyph_set
521                && same_type.len() < ENTRIES_PER_FORMAT
522                && same_type.iter().all(|kept| kept.key != entry.key)
523            {
524                fonts.push(entry);
525            }
526        }
527        self.fonts = fonts;
528
529        let (encoded, checksums) = self.encode();
530        let identity = Some((checksums, encoded.len()));
531        // ponytail: a CRC32 collision skips one write; the old bytes stay valid for their own
532        // keys, so output is unaffected and the next change rewrites them.
533        if self.loaded == identity {
534            return None;
535        }
536        self.loaded = identity;
537        Some(encoded)
538    }
539
540    /// The file bytes and their section checksums (glyphs, fonts).
541    fn encode(&self) -> (Vec<u8>, [u32; 2]) {
542        let mut out = Writer::default();
543        out.0.extend_from_slice(MAGIC);
544        out.u32(FORMAT_VERSION);
545        out.bytes(ENGINE_VERSION.as_bytes());
546        out.array(&self.fingerprint);
547        let empty = Glyphs::default();
548        let glyphs = self.glyphs().unwrap_or(&empty);
549        let glyph_checksum = section(&mut out, |out| encode_glyphs(out, glyphs));
550        let font_checksum = section(&mut out, |out| {
551            out.len(self.fonts.len());
552            for entry in &self.fonts {
553                out.u8(font_type_tag(entry.font.font_type()));
554                out.array(&entry.key);
555                out.array(&entry.glyph_set);
556                out.bytes(entry.font.bytes());
557            }
558        });
559        (out.0, [glyph_checksum, font_checksum])
560    }
561
562    /// Check the header and decode the font section. Returns both section checksums and the
563    /// glyph section's range, which is left for [`Self::glyphs`].
564    fn decode(&mut self, bytes: &[u8]) -> Option<([u32; 2], Range<usize>)> {
565        let mut input = Reader(bytes);
566        (input.take(MAGIC.len())? == MAGIC).then_some(())?;
567        (input.u32()? == FORMAT_VERSION).then_some(())?;
568        (input.bytes()? == ENGINE_VERSION.as_bytes()).then_some(())?;
569        (input.array::<16>()? == self.fingerprint).then_some(())?;
570
571        let glyph_checksum = input.u32()?;
572        let glyph_len = input.u32()? as usize;
573        let glyph_start = bytes.len() - input.0.len();
574        input.take(glyph_len)?;
575
576        let font_checksum = input.u32()?;
577        let font_len = input.u32()? as usize;
578        let mut fonts = Reader(input.take(font_len)?);
579        input.0.is_empty().then_some(())?;
580        (checksum(fonts.0) == font_checksum).then_some(())?;
581        for _ in 0..fonts.len(33)? {
582            let font_type = *FONT_TYPES.get(fonts.u8()? as usize)?;
583            let key = fonts.array()?;
584            let glyph_set = fonts.array()?;
585            let font = CachedFont::decode(font_type, fonts.bytes()?.to_vec())?;
586            self.fonts.push(FontEntry {
587                key,
588                glyph_set,
589                font,
590            });
591        }
592        fonts.0.is_empty().then_some(())?;
593        Some((
594            [glyph_checksum, font_checksum],
595            glyph_start..glyph_start + glyph_len,
596        ))
597    }
598}
599
600/// Write one section: its checksum and length, then the bytes `write` produces. The body goes
601/// straight into `out` and the frame is filled in afterwards, so nothing is copied. Returns the
602/// checksum, which catches corruption the structure checks can't (a flipped byte inside a font).
603fn section(out: &mut Writer, write: impl FnOnce(&mut Writer)) -> u32 {
604    let frame = out.0.len();
605    out.u32(0);
606    out.u32(0);
607    let start = out.0.len();
608    write(out);
609    let body_checksum = checksum(&out.0[start..]);
610    let len = u32::try_from(out.0.len() - start).expect("cache sections stay below 4 GiB");
611    out.0[frame..frame + 4].copy_from_slice(&body_checksum.to_le_bytes());
612    out.0[frame + 4..start].copy_from_slice(&len.to_le_bytes());
613    body_checksum
614}
615
616fn encode_glyphs(out: &mut Writer, glyphs: &Glyphs) {
617    // Always set after a store: preparation computes it before anything is persisted.
618    out.array(&glyphs.processed_signature.unwrap_or_default());
619
620    // Sorted so identical content always encodes identically (lets unchanged runs skip writes).
621    let mut parsed: Vec<_> = glyphs.parsed.iter().collect();
622    parsed.sort_unstable_by_key(|(hash, _)| **hash);
623    out.len(parsed.len());
624    for (hash, glyph) in parsed {
625        out.array(hash);
626        out.f64(glyph.height);
627        out.f64(glyph.width);
628        out.len(glyph.paths.len());
629        for path in &glyph.paths {
630            out.tiny_path(path);
631        }
632        out.parsed_color_layers(glyph.color_layers.as_deref());
633    }
634
635    let mut processed: Vec<_> = glyphs.processed.iter().collect();
636    processed.sort_unstable_by_key(|(hash, _)| **hash);
637    out.len(processed.len());
638    for (hash, glyph) in processed {
639        out.array(hash);
640        out.processed_glyph(glyph);
641    }
642
643    let mut variant_processed: Vec<_> = glyphs.variant_processed.iter().collect();
644    variant_processed.sort_unstable_by_key(|(hash, _)| **hash);
645    out.len(variant_processed.len());
646    for (hash, entries) in variant_processed {
647        out.array(hash);
648        let mut entries: Vec<_> = entries.iter().collect();
649        entries.sort_unstable_by_key(|(advance, _)| *advance);
650        out.len(entries.len());
651        for (advance, glyph) in entries {
652            out.u64(*advance);
653            out.processed_glyph(glyph);
654        }
655    }
656}
657
658fn decode_glyphs(section: &[u8]) -> Option<Glyphs> {
659    let mut input = Reader(section);
660    let mut glyphs = Glyphs {
661        processed_signature: Some(input.array()?),
662        ..Default::default()
663    };
664    for _ in 0..input.len(16)? {
665        let hash = input.array()?;
666        let height = input.f64()?;
667        let width = input.f64()?;
668        let mut paths = Vec::new();
669        for _ in 0..input.len(8)? {
670            paths.push(input.tiny_path()?);
671        }
672        let color_layers = input.parsed_color_layers(paths.len())?;
673        glyphs.parsed.insert(
674            hash,
675            Arc::new(CachedGlyph {
676                color_layers,
677                height,
678                paths,
679                width,
680            }),
681        );
682    }
683    for _ in 0..input.len(16)? {
684        let hash = input.array()?;
685        glyphs.processed.insert(hash, input.processed_glyph()?);
686    }
687    for _ in 0..input.len(16)? {
688        let hash = input.array()?;
689        let mut entries = Vec::new();
690        for _ in 0..input.len(8)? {
691            entries.push((input.u64()?, input.processed_glyph()?));
692        }
693        glyphs.variant_processed.insert(hash, entries);
694    }
695    input.0.is_empty().then_some(glyphs)
696}
697
698/// Whether `path` is a cache file (checked by its name and header) or a temporary file left by an
699/// interrupted cache write (checked by its exact generated name).
700#[cfg(feature = "fs")]
701fn created_by_cache(path: &Path) -> bool {
702    let name = path.file_name().unwrap_or_default().to_string_lossy();
703    if let Some(stem) = name.strip_suffix(&format!(".{EXTENSION}")) {
704        cache_stem(stem).is_some() && has_cache_header(path)
705    } else {
706        temp_file_stem(&name).is_some_and(|stem| cache_stem(stem).is_some())
707    }
708}
709
710#[cfg(feature = "fs")]
711/// The font prefix of a cache file stem shaped `<prefix>.<16 hex digits>`.
712fn cache_stem(stem: &str) -> Option<&str> {
713    let (prefix, hash) = stem.rsplit_once('.')?;
714    (hash.len() == 16 && hash.bytes().all(|byte| byte.is_ascii_hexdigit())).then_some(prefix)
715}
716
717#[cfg(feature = "fs")]
718/// The cache file stem of a temporary file named `<stem>.wfcache.<pid>-<write>.tmp`, as `write`
719/// creates them.
720fn temp_file_stem(name: &str) -> Option<&str> {
721    let (stem, suffix) = name
722        .strip_suffix(".tmp")?
723        .rsplit_once(&format!(".{EXTENSION}."))?;
724    let (pid, write) = suffix.split_once('-')?;
725    let digits = |part: &str| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit());
726    (digits(pid) && digits(write)).then_some(stem)
727}
728
729#[cfg(feature = "fs")]
730/// Whether `path` starts with the cache magic. Only the header is read, however large the file is.
731fn has_cache_header(path: &Path) -> bool {
732    let mut header = [0; MAGIC.len()];
733    File::open(path)
734        .and_then(|mut file| file.read_exact(&mut header))
735        .is_ok_and(|()| &header == MAGIC)
736}
737
738/// The `cacheLocation` file for one generation.
739#[cfg(feature = "fs")]
740pub(crate) struct DiskCache {
741    dir: PathBuf,
742    file_name: String,
743    font_prefix: String,
744    state: CacheState,
745}
746
747#[cfg(feature = "fs")]
748impl Deref for DiskCache {
749    type Target = CacheState;
750
751    fn deref(&self) -> &CacheState {
752        &self.state
753    }
754}
755
756#[cfg(feature = "fs")]
757impl DiskCache {
758    pub(crate) fn open(
759        dir: &str,
760        options: &ResolvedGenerationOptions,
761        sources: &[LoadedSvgSource],
762    ) -> Self {
763        Self::load(dir, &options.font_name, CacheState::new(options, sources))
764    }
765
766    pub(crate) fn open_variant<Files: AsRef<[LoadedSvgSource]>>(
767        dir: &str,
768        options: &ResolvedGenerationOptions,
769        variants: &ResolvedVariants,
770        family: &VariantFamilySources<Files>,
771    ) -> Self {
772        let state = CacheState::new_variant(options, variants, family);
773        Self::load(dir, &options.font_name, state)
774    }
775
776    fn load(dir: &str, font_name: &str, mut state: CacheState) -> Self {
777        let dir = PathBuf::from(dir);
778        let font_prefix = sanitize(font_name);
779        let file_name = format!(
780            "{font_prefix}.{}.{EXTENSION}",
781            &hex(&state.fingerprint)[..16]
782        );
783        if let Ok(bytes) = fs::read(dir.join(&file_name)) {
784            state.load(bytes);
785        }
786        Self {
787            dir,
788            file_name,
789            font_prefix,
790            state,
791        }
792    }
793
794    /// Persist the updated cache. Best effort: an unwritable location only costs the next run its
795    /// warm start.
796    pub(crate) fn store(
797        mut self,
798        sources: &[LoadedSvgSource],
799        glyph_cache: &GlyphCache,
800        outputs: &FontOutputs,
801        types: &[FontType],
802    ) {
803        let bytes = self.state.update(sources, glyph_cache, outputs, types);
804        self.save(bytes);
805    }
806
807    /// Variant counterpart of [`Self::store`].
808    pub(crate) fn store_variants<Files: AsRef<[LoadedSvgSource]>>(
809        mut self,
810        variants: &[Files],
811        glyph_cache: &VariantGlyphCache,
812        outputs: &FontOutputs,
813        types: &[FontType],
814    ) {
815        let bytes = self
816            .state
817            .update_variants(variants, glyph_cache, outputs, types);
818        self.save(bytes);
819    }
820
821    fn save(self, bytes: Option<Vec<u8>>) {
822        let Self {
823            dir,
824            file_name,
825            font_prefix,
826            ..
827        } = self;
828        if let Some(bytes) = bytes {
829            let path = dir.join(&file_name);
830            match write(&dir, &file_name, &bytes) {
831                Ok(()) => prune_siblings(&dir, &file_name, &font_prefix),
832                // The cache never removes what it didn't create, so this stays cold until the user
833                // removes the directory; say so instead of failing silently on every run.
834                Err(_) if path.is_dir() => eprintln!(
835                    "warning: webfont-generator: {} is a directory, so the generation cache can't be saved there; remove it to re-enable caching",
836                    path.display()
837                ),
838                Err(_) => {}
839            }
840        }
841    }
842}
843
844#[cfg(feature = "fs")]
845fn write(dir: &Path, file_name: &str, bytes: &[u8]) -> Result<()> {
846    fs::create_dir_all(dir)?;
847    // Unique per write within this process, so concurrent generations never share a file.
848    static WRITES: AtomicU64 = AtomicU64::new(0);
849    let write = WRITES.fetch_add(1, Relaxed);
850    let tmp = dir.join(format!("{}.{}-{write}.tmp", file_name, process::id()));
851    fs::write(&tmp, bytes)?;
852    fs::rename(&tmp, dir.join(file_name)).inspect_err(|_| {
853        let _ = fs::remove_file(&tmp);
854    })
855}
856
857/// Drop older files for the same font name: they belong to superseded glyph settings.
858// ponytail: two configs sharing a font name but differing in glyph settings evict each other
859// (each run is then cold). Key the prefix by more than the font name if that shows up.
860#[cfg(feature = "fs")]
861fn prune_siblings(dir: &Path, file_name: &str, font_prefix: &str) {
862    for entry in fs::read_dir(dir).into_iter().flatten().flatten() {
863        let name = entry.file_name();
864        let name = name.to_string_lossy();
865        let sibling = name
866            .strip_suffix(&format!(".{EXTENSION}"))
867            .and_then(cache_stem)
868            .is_some_and(|prefix| prefix == font_prefix);
869        if sibling
870            && name != file_name
871            && entry.file_type().is_ok_and(|kind| kind.is_file())
872            && has_cache_header(&entry.path())
873        {
874            let _ = fs::remove_file(entry.path());
875        }
876    }
877}
878
879/// Digest of the font-affecting settings each format depends on, in `FONT_TYPES` order: the SVG
880/// options (part of every glyph), plus the TTF options for formats built from the TTF, plus the
881/// format's own options.
882fn format_settings_digests(settings: &Value) -> [[u8; 16]; 5] {
883    FONT_TYPES.map(|font_type| {
884        let own: &[&str] = match font_type {
885            FontType::Svg => &[],
886            FontType::Ttf | FontType::Eot => &["ttf"],
887            FontType::Woff => &["ttf", "woff"],
888            FontType::Woff2 => &["ttf", "woff2"],
889        };
890        digest(&with_format_options(settings, own))
891    })
892}
893
894/// `settings` with only the SVG options plus the `own` format options. Empty option objects are
895/// dropped, so leaving `formatOptions` (or one format's options) unset and setting it to nothing
896/// produce the same identity.
897fn with_format_options(settings: &Value, own: &[&str]) -> Value {
898    let mut settings = settings.clone();
899    if let Some(object) = settings.as_object_mut()
900        && let Some(formats) = object
901            .get_mut("formatOptions")
902            .and_then(Value::as_object_mut)
903    {
904        formats.retain(|name, options| {
905            (name == "svg" || own.contains(&name.as_str()))
906                && options
907                    .as_object()
908                    .is_none_or(|options| !options.is_empty())
909        });
910        if formats.is_empty() {
911            object.remove("formatOptions");
912        }
913    }
914    settings
915}
916
917/// Key for each format's bytes: the settings that format depends on, plus the ordered glyph names,
918/// codepoints, and source content hashes.
919fn font_keys(settings_digests: &[[u8; 16]; 5], glyph_set: &[u8; 16]) -> [[u8; 16]; 5] {
920    from_fn(|index| {
921        digest(&(
922            FORMAT_VERSION,
923            ENGINE_VERSION,
924            FONT_TYPES[index].as_extension(),
925            settings_digests[index],
926            glyph_set,
927        ))
928    })
929}
930
931/// CRC32 of the file body, verified before decoding.
932fn checksum(bytes: &[u8]) -> u32 {
933    let mut crc = Crc::new();
934    crc.update(bytes);
935    crc.sum()
936}
937
938/// MD5 of a value's JSON serialization, for identities and keys.
939fn digest(value: &impl Serialize) -> [u8; 16] {
940    compute(to_vec(value).expect("cache identity serializes")).0
941}
942
943/// Identifies the ordered glyph names, codepoints, and source contents.
944pub(crate) fn glyph_set(
945    options: &ResolvedGenerationOptions,
946    sources: &[LoadedSvgSource],
947) -> [u8; 16] {
948    let glyphs: Vec<Value> = sources
949        .iter()
950        .map(|source| {
951            json!([
952                source.glyph_name,
953                options.codepoints.get(&source.glyph_name),
954                hex(&source_content_hash(&source.contents)),
955            ])
956        })
957        .collect();
958    digest(&glyphs)
959}
960
961/// Identifies a variant family's ordered glyph names and codepoints, and the source contents
962/// (or blank) each design uses for them, which also captures missing-glyph resolution.
963pub(crate) fn variant_glyph_set<Files: AsRef<[LoadedSvgSource]>>(
964    family: &VariantFamilySources<Files>,
965) -> [u8; 16] {
966    let glyphs: Vec<Value> = family
967        .glyphs
968        .iter()
969        .map(|glyph| {
970            let designs: Vec<Option<String>> = glyph
971                .sources
972                .iter()
973                .map(|source| match source {
974                    Some(VariantGlyphSource::Source {
975                        variant_index,
976                        source_index,
977                    }) => Some(hex(&source_content_hash(
978                        &family.variants[*variant_index].as_ref()[*source_index].contents,
979                    ))),
980                    _ => None,
981                })
982                .collect();
983            json!([glyph.name, glyph.codepoint, designs])
984        })
985        .collect();
986    digest(&glyphs)
987}
988
989#[cfg(feature = "fs")]
990fn sanitize(font_name: &str) -> String {
991    font_name
992        .chars()
993        .map(|char| {
994            if char.is_ascii_alphanumeric() || char == '-' || char == '_' {
995                char
996            } else {
997                '_'
998            }
999        })
1000        .collect()
1001}
1002
1003fn hex(bytes: &[u8]) -> String {
1004    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
1005}