write_fonts/
validate.rs

1//! The pre-compilation validation pass
2
3use std::{
4    collections::BTreeSet,
5    fmt::{Debug, Display},
6    ops::Deref,
7};
8
9use crate::offsets::{NullableOffsetMarker, OffsetMarker};
10
11/// Pre-compilation validation of tables.
12///
13/// The OpenType specification describes various requirements for different
14/// tables that are awkward to encode in the type system, such as requiring
15/// certain arrays to have equal lengths. These requirements are enforced
16/// via a validation pass.
17pub trait Validate {
18    /// Ensure that this table is well-formed, reporting any errors.
19    ///
20    /// This is an auto-generated method that calls to [validate_impl][Self::validate_impl] and
21    /// collects any errors.
22    fn validate(&self) -> Result<(), ValidationReport> {
23        let mut ctx = Default::default();
24        self.validate_impl(&mut ctx);
25        if ctx.errors.is_empty() {
26            Ok(())
27        } else {
28            Err(ValidationReport { errors: ctx.errors })
29        }
30    }
31
32    /// Validate this table.
33    ///
34    /// If you need to implement this directly, it should look something like:
35    ///
36    /// ```rust
37    /// # use write_fonts::validate::{Validate, ValidationCtx};
38    /// struct MyRecord {
39    ///     my_values: Vec<u16>,
40    /// }
41    ///
42    /// impl Validate for MyRecord {
43    ///     fn validate_impl(&self, ctx: &mut ValidationCtx) {
44    ///         ctx.in_table("MyRecord", |ctx| {
45    ///             ctx.in_field("my_values", |ctx| {
46    ///                 if self.my_values.len() > (u16::MAX as usize) {
47    ///                     ctx.report("array is too long");
48    ///                 }
49    ///             })
50    ///         })
51    ///     }
52    /// }
53    /// ```
54    #[allow(unused_variables)]
55    fn validate_impl(&self, ctx: &mut ValidationCtx);
56}
57
58/// A context for collecting validation error.
59///
60/// This is responsible for tracking the position in the tree at which
61/// a given error is reported.
62///
63/// ## paths/locations
64///
65/// As validation travels down through the object graph, the path is recorded
66/// via appropriate calls to methods like [in_table][Self::in_table] and [in_field][Self::in_field].
67#[derive(Clone, Debug, Default)]
68pub struct ValidationCtx {
69    cur_location: Vec<LocationElem>,
70    errors: Vec<ValidationError>,
71}
72
73#[derive(Debug, Clone)]
74struct ValidationError {
75    error: String,
76    location: Vec<LocationElem>,
77}
78
79/// One or more validation errors.
80#[derive(Clone)]
81pub struct ValidationReport {
82    errors: Vec<ValidationError>,
83}
84
85#[derive(Debug, Clone)]
86enum LocationElem {
87    Table(&'static str),
88    Field(&'static str),
89    Index(usize),
90}
91
92impl ValidationCtx {
93    /// Run the provided closer in the context of a new table.
94    ///
95    /// Errors reported in the closure will include the provided identifier
96    /// in their path.
97    pub fn in_table(&mut self, name: &'static str, f: impl FnOnce(&mut ValidationCtx)) {
98        self.with_elem(LocationElem::Table(name), f);
99    }
100
101    /// Run the provided closer in the context of a new field.
102    ///
103    /// Errors reported in the closure will be associated with the field.
104    pub fn in_field(&mut self, name: &'static str, f: impl FnOnce(&mut ValidationCtx)) {
105        self.with_elem(LocationElem::Field(name), f);
106    }
107
108    /// Run the provided closer for each item in an array.
109    ///
110    /// This handles tracking the active item, so that validation errors can
111    /// be associated with the correct index.
112    pub fn with_array_items<'a, T: 'a>(
113        &mut self,
114        iter: impl Iterator<Item = &'a T>,
115        mut f: impl FnMut(&mut ValidationCtx, &T),
116    ) {
117        for (i, item) in iter.enumerate() {
118            self.with_elem(LocationElem::Index(i), |ctx| f(ctx, item))
119        }
120    }
121
122    /// Report a new error, associating it with the current path.
123    pub fn report(&mut self, msg: impl Display) {
124        self.errors.push(ValidationError {
125            location: self.cur_location.clone(),
126            error: msg.to_string(),
127        });
128    }
129
130    fn with_elem(&mut self, elem: LocationElem, f: impl FnOnce(&mut ValidationCtx)) {
131        self.cur_location.push(elem);
132        f(self);
133        self.cur_location.pop();
134    }
135}
136
137impl Display for ValidationReport {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        if self.errors.len() == 1 {
140            return writeln!(f, "Validation error:\n{}", self.errors.first().unwrap());
141        }
142
143        writeln!(f, "{} validation errors:", self.errors.len())?;
144        for (i, error) in self.errors.iter().enumerate() {
145            writeln!(f, "#{}\n{error}", i + 1)?;
146        }
147        Ok(())
148    }
149}
150
151impl Debug for ValidationReport {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        <Self as Display>::fmt(self, f)
154    }
155}
156
157static MANY_SPACES: &str = "                                                                                                        ";
158
159impl Display for ValidationError {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        writeln!(f, "\"{}\"", self.error)?;
162        let mut indent = 0;
163        if self.location.len() < 2 {
164            return f.write_str("no parent location available");
165        }
166
167        for (i, window) in self.location.windows(2).enumerate() {
168            let prev = &window[0];
169            let current = &window[1];
170            if i == 0 {
171                if let LocationElem::Table(name) = prev {
172                    write!(f, "in: {name}")?;
173                } else {
174                    panic!("first item always table");
175                }
176            }
177
178            match current {
179                LocationElem::Table(name) => {
180                    indent += 1;
181                    let indent_str = &MANY_SPACES[..indent * 2];
182                    write!(f, "\n{indent_str}{name}")
183                }
184                LocationElem::Field(name) => write!(f, ".{name}"),
185                LocationElem::Index(idx) => write!(f, "[{idx}]"),
186            }?;
187        }
188        writeln!(f)
189    }
190}
191
192impl<T: Validate> Validate for Vec<T> {
193    fn validate_impl(&self, ctx: &mut ValidationCtx) {
194        ctx.with_array_items(self.iter(), |ctx, item| item.validate_impl(ctx))
195    }
196}
197
198impl<const N: usize, T: Validate> Validate for OffsetMarker<T, N> {
199    fn validate_impl(&self, ctx: &mut ValidationCtx) {
200        self.deref().validate_impl(ctx)
201    }
202}
203
204impl<const N: usize, T: Validate> Validate for NullableOffsetMarker<T, N> {
205    fn validate_impl(&self, ctx: &mut ValidationCtx) {
206        if let Some(b) = self.as_ref() {
207            b.validate_impl(ctx);
208        }
209    }
210}
211
212impl<T: Validate> Validate for Option<T> {
213    fn validate_impl(&self, ctx: &mut ValidationCtx) {
214        if let Some(t) = self {
215            t.validate_impl(ctx)
216        }
217    }
218}
219
220impl<T: Validate> Validate for BTreeSet<T> {
221    fn validate_impl(&self, ctx: &mut ValidationCtx) {
222        ctx.with_array_items(self.iter(), |ctx, item| item.validate_impl(ctx))
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn sanity_check_array_validation() {
232        #[derive(Clone, Debug, Copy)]
233        struct Derp(i16);
234
235        struct DerpStore {
236            derps: Vec<Derp>,
237        }
238
239        impl Validate for Derp {
240            fn validate_impl(&self, ctx: &mut ValidationCtx) {
241                if self.0 > 7 {
242                    ctx.report("this derp is too big!!");
243                }
244            }
245        }
246
247        impl Validate for DerpStore {
248            fn validate_impl(&self, ctx: &mut ValidationCtx) {
249                ctx.in_table("DerpStore", |ctx| {
250                    ctx.in_field("derps", |ctx| self.derps.validate_impl(ctx))
251                })
252            }
253        }
254
255        let my_derps = DerpStore {
256            derps: [1i16, 0, 3, 4, 12, 7, 6].into_iter().map(Derp).collect(),
257        };
258
259        let report = my_derps.validate().err().unwrap();
260        assert_eq!(report.errors.len(), 1);
261        // ensure that the index is being calculated correctly
262        assert!(report.to_string().contains(".derps[4]"));
263    }
264}