Skip to main content

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