tfm/pl/
error.rs

1//! Warnings relating to property list file parsing
2
3use crate::{ligkern::InfiniteLoopError, NextLargerProgramWarning};
4
5/// Warning generated while parsing a property list file.
6#[derive(PartialEq, Eq, Debug, Clone)]
7pub struct ParseWarning {
8    /// Location of the part of the property list file that generates this warning.
9    ///
10    /// The span is in number of Unicode code points, not bytes.
11    pub span: std::ops::Range<usize>,
12    /// Point in the file that Knuth's pltotf flags as problematic.
13    ///
14    /// Most warnings in pltotf are printed with a context line.
15    /// This looks like this:
16    /// ```txt
17    /// PARAMETER index must not be zero (line 9).
18    /// (PARAMETER D 0
19    ///                D 5)  
20    /// ```
21    /// The context line is printed over 2 lines in the terminal,
22    ///     with the break occurring at the part of the line that triggered the warning.
23    ///
24    /// This field gives the offset of the break within the entire file.
25    /// The offset is in number of Unicode code points, not bytes.
26    /// For warnings that don't have a context line (like next larger warnings)
27    ///     this field has a value of [`None`].
28    pub knuth_pltotf_offset: Option<usize>,
29    /// Kind of the warning.
30    pub kind: ParseWarningKind,
31}
32
33/// Kind of parse warning.
34#[derive(PartialEq, Eq, Debug, Clone)]
35pub enum ParseWarningKind {
36    /// The file contains an opening parenthesis that is not followed/balanced by a closing parenthesis.
37    UnbalancedOpeningParenthesis {
38        opening_parenthesis_span: std::ops::Range<usize>,
39    },
40    /// The file contains a closing parenthesis that is not preceded/balanced by an opening parenthesis.
41    UnexpectedClosingParenthesis,
42    /// There is some junk in the middle of a property list;
43    ///     e.g. `(CHARWD D 9.0) junk (CHARDP D 8.0)`.
44    JunkInsidePropertyList { junk: String },
45    /// There is some junk after a property value;
46    ///     e.g. `(CHECKSUM O 123 junk)`.
47    JunkAfterPropertyValue { junk: String },
48    /// A parameter number has value 255 which is too big.
49    ///
50    /// Due to how pltotf processes numbers, parameter numbers larger than
51    ///     255 are reported with [ParseWarningKind::SmallIntegerIsTooBig]
52    ///     and [ParseWarningKind::ParameterNumberIsZero] warnings.
53    ParameterNumberIsTooBig,
54    /// A parameter number has value 0 which is invalid as parameter numbers must be strictly positive.
55    ParameterNumberIsZero,
56    /// A small integer is bigger than 255.
57    SmallIntegerIsTooBig { radix: u8 },
58    /// An integer is bigger than 255^4.
59    IntegerIsTooBig { radix: u8 },
60    /// A decimal number is bigger than 2048 in absolute value.
61    DecimalNumberIsTooBig,
62    /// A property name is invalid.
63    InvalidPropertyName {
64        /// The name that was provided.
65        provided_name: String,
66        /// Names that are allowed in this position.
67        allowed_property_names: &'static [&'static str],
68    },
69    /// Invalid prefix for an integer.
70    InvalidPrefixForInteger { prefix: Option<char> },
71    /// Invalid prefix for a small integer.
72    InvalidPrefixForSmallInteger,
73    /// Invalid prefix for a decimal number.
74    InvalidPrefixForDecimalNumber,
75    /// Invalid octal digit.
76    InvalidOctalDigit { invalid_digit: char },
77    /// No character value is provided.
78    EmptyCharacterValue,
79    /// Invalid face code
80    InvalidFaceCode,
81    /// Invalid boolean value.
82    InvalidBoolean,
83    /// A header index is smaller than 18
84    HeaderIndexIsTooSmall,
85    /// The lig table is too big (todo: larger than what?)
86    LigTableIsTooBig,
87    /// The lig/kern program contains a cycle
88    CycleInLigKernProgram(InfiniteLoopError),
89    /// The next larger program contains a cycle
90    CycleInNextLargerProgram(NextLargerProgramWarning),
91    /// The file is really seven bit safe, even though it says it is
92    NotReallySevenBitSafe,
93    /// The design size is less than 1.
94    DesignSizeIsTooSmall,
95    /// The file contains a non-visible ASCII character.
96    NonVisibleAsciiCharacter { character: char },
97}
98
99struct Data {
100    rule: String,
101    problem: String,
102    action: &'static str,
103    pltotf_message: String,
104    pltotf_section: (u8, u8),
105}
106
107impl ParseWarning {
108    pub fn pltotf_message(&self, pl_source: &str) -> String {
109        let data = self.kind.data();
110        match self.knuth_pltotf_offset {
111            None => data.pltotf_message,
112            Some(pltotf_point) => {
113                add_pltotf_error_context(pl_source, data.pltotf_message, pltotf_point)
114            }
115        }
116    }
117}
118
119/* TODO
120impl ParseError {
121    fn pltotf_section(&self) -> (u8, u8) {
122        use ParseError::*;
123        match self {
124            // TODO
125            DecimalTooLarge { .. } => (64, 1),
126            LigTableTooLong { .. } => (101, 1),
127            NotReallySevenBitSafe => (110, 1),
128            _ => (0, 0)
129        }
130    }
131}
132 */
133
134impl ParseWarningKind {
135    fn data(&self) -> Data {
136        use ParseWarningKind::*;
137        match self.clone() {
138            EmptyCharacterValue => Data {
139                rule: "an ASCII character must be specified after a \"C\"".into(),
140                problem: "empty".into(),
141                action: "",
142                pltotf_message: "\"C\" value must be standard ASCII and not a paren".into(),
143                pltotf_section: (52,1),
144            },
145            InvalidBoolean => Data {
146                rule: "boolean values must start with T, t, F or f.".into(),
147                problem: "invalid boolean value".into(),
148                action: "this property list element will be skipped",
149                pltotf_message: "The flag value should be \"TRUE\" or \"FALSE\"".into(),
150                pltotf_section: (90, 1),
151            },
152            InvalidOctalDigit{ invalid_digit } => Data {
153                rule: "octal numbers can only contain the characters 0 through 7 inclusive".into(),
154                problem: format!["octal number contains the non-octal character {invalid_digit}"],
155                action: "the octal number will be truncated to before the problematic character",
156                pltotf_message: "Illegal digit".into(),
157                pltotf_section: (60, 2),
158            },
159            NonVisibleAsciiCharacter{ character: c } => Data {
160                rule: "property list files can only contain newlines and printable ASCII characters (space through tilde inclusive)".into(),
161                problem: format!["invalid character {} (U+{:04X})", c, c as u32] ,
162                action: "this character will be ignored",
163                pltotf_message: if (c as usize) < 128 { "Illegal character in the file".into()} else {"".into()},
164                pltotf_section: (32, 1),
165            },
166            UnbalancedOpeningParenthesis { opening_parenthesis_span: _ } => Data {
167                rule: "all opening parentheses must be balanced by a subsequent closing parenthesis".into(),
168                problem: "opening parenthesis is never closed".into(),
169                action: "an additional closing parenthesis will be appended to the file",
170                pltotf_message:  "File ended unexpectedly: No closing \")\"".into(),
171                pltotf_section: (33,1),
172            },
173            JunkInsidePropertyList { junk } => Data {
174                rule: "".into(),
175                problem: format!["junk string '{junk}' in the middle of a property list"],
176                action: "this junk will be ignored",
177                pltotf_message: "There's junk here that is not in parentheses".into(),
178                pltotf_section: (83,1),
179            },
180            JunkAfterPropertyValue { junk } => Data {
181                rule: "".into(),
182                problem: format!["junk string '{junk}' after a property list value"],
183                action: "this junk will be ignored",
184                pltotf_message: "Junk after property value will be ignored".into(),
185                pltotf_section: (35,1),
186            },
187            UnexpectedClosingParenthesis => Data {
188                rule: "all closing parentheses must be balanced by a preceding opening parenthesis".into(),
189                problem: "closing parenthesis was never opened".into(),
190                action: "the closing parenthesis will be ignored",
191                pltotf_message:  "Extra right parenthesis".into(),
192                pltotf_section: (82, 1),
193            },
194            InvalidPrefixForSmallInteger => Data {
195                rule: "8-bit integers must be prefixed by C, D, F, H or O".into(),
196                problem: "invalid prefix for an 8-bit integer".into(),
197                action: "0 will be used instead",
198                pltotf_message: "You need \"C\" or \"D\" or \"O\" or \"H\" or \"F\" here".into(),
199                pltotf_section: (51,1),
200            },
201            InvalidPrefixForInteger{ prefix: _ } => Data {
202                rule: "32-bit integers must be prefixed by H or O".into(),
203                problem: "invalid prefix for an 32-bit integer".into(),
204                action: "0 will be used instead",
205                pltotf_message: "An octal (\"O\") or hex (\"H\") value is needed here".into(),
206                pltotf_section: (59,1),
207            },
208            InvalidPropertyName {
209                provided_name,
210                allowed_property_names,
211            } => {
212                let allowed_property_names = {
213                    let mut v: Vec<&'static str> = allowed_property_names.to_vec();
214                    v.sort();
215                    v
216                };
217                Data {
218                    rule: format![
219                        "the following property names are allowed here: {}",
220                        allowed_property_names.join(", ")
221                    ],
222                    problem: format!("the property name {provided_name} is not allowed here"),
223                    action: "this element will be ignored",
224                    pltotf_message: "Sorry, I don't know that property name".into(),
225                    pltotf_section: (49, 1),
226                }
227            }
228            CycleInNextLargerProgram(warning) => {
229                let (rule, problem, action) = match warning {
230                    NextLargerProgramWarning::NonExistentCharacter { original, next_larger } => (
231                        "next larger rules must reference characters defined in the file",
232                        format!["the next larger rule for character {original} references an undefined character {next_larger}"],
233                        "the element flagged here will be ignored",
234                    ),
235                    NextLargerProgramWarning::InfiniteLoop{ original, next_larger } => (
236                        "the next larger rules cannot create cycles (e.g., X larger than Y and Y larger than X)",
237                        format!("setting {} as the next larger character for {} creates a cycle", next_larger, original),
238                        "the element flagged here will be skipped in order to break the cycle",
239                    ),
240                };
241                Data {
242                    rule: rule.into(),
243                    problem,
244                    action,
245                    pltotf_message: warning.pltotf_message(),
246                    pltotf_section: (warning.pltotf_section(), 1),
247                }
248            }
249            ParameterNumberIsZero => Data {
250                rule: "parameter indices must be integers in the range [1,254] inclusive".into(),
251                problem: "0 is not a valid parameter index".into(),
252                action: "this parameter element will be ignored",
253                pltotf_message: "PARAMETER index must not be zero".into(),
254                pltotf_section: (93, 1),
255            },
256            ParameterNumberIsTooBig => Data {
257                rule: "parameter indices must be integers in the range [1,254] inclusive".into(),
258                problem: "this parameter index is too big".into(),
259                action: "this parameter element will be ignored",
260                pltotf_message: "This PARAMETER index is too big for my present table size".into(),
261                pltotf_section: (93, 2),
262            },
263            SmallIntegerIsTooBig { radix } => Data {
264                rule: format!["8-bit numbers can be at most 2^8 = {} = 0x{:x} = 0o{:o}", u8::MAX, u8::MAX, u8::MAX],
265                problem: "this number is too big".into(),
266                action: "0 will be used instead",
267                pltotf_message: {
268                    let max = match radix {
269                        8 => format!["'{:o}", u8::MAX],
270                        10 => format!["{}", u8::MAX],
271                        _ => format!["\"{:X}", u8::MAX],
272                    };
273                    format!["This value shouldn't exceed {max}"]
274                },
275                pltotf_section: match radix {
276                    8 => (54, 1),
277                    10 => (53, 1),
278                    _ => (55, 1),
279                },
280            },
281            IntegerIsTooBig { radix, } => Data {
282              rule: format!["32-bit numbers can be at most 2^32 = {} = 0x{:x} = 0o{:o}", u32::MAX, u32::MAX, u32::MAX],
283              problem: format!["this {} number is too big", if radix == 8 {
284                "octal"
285              } else {
286                "hexadecimal"
287              }],
288              action: "0 will be used instead",
289              pltotf_message: if radix == 8 {
290                  format!["Sorry, the maximum octal value is O {:o}", u32::MAX]
291              } else {
292                  format!["Sorry, the maximum hex value is H {:X}", u32::MAX]
293              },
294              pltotf_section: (60, 1),
295            },
296            InvalidFaceCode => Data{
297                rule: "face codes must satisfy the pattern [BLM][IR][CER]".into(),
298                problem: "invalid face code".into(),
299                action: "the face code MRR will be used instead",
300                pltotf_message: "Illegal face code, I changed it to MRR".into(),
301                pltotf_section: (56, 1),
302            },
303            CycleInLigKernProgram(err) => Data {
304                rule: "ligature programs cannot contain cycles".into(),
305                problem: format!["this ligature rule for the pair ({},{}) creates a cycle",
306                match err.starting_pair.0 {
307                  None => "boundary".to_string(),
308                  Some(c) => format!["{}", c],
309                },
310                err.starting_pair.1,
311                ],
312                action: "all ligatures will be deleted",
313                pltotf_message: format![
314                    "Infinite ligature loop starting with {} and '{:o}!\nAll ligatures will be cleared.",
315                    match err.starting_pair.0 {
316                        None => "boundary".to_string(),
317                        Some(c) => format!["'{:o}", c.0],
318                    },
319                    err.starting_pair.1.0,
320                ],
321                pltotf_section: (125, 1),
322            },
323            InvalidPrefixForDecimalNumber => Data{
324                rule: "decimal numbers must be prefixed by D or R".into(),
325                problem: "invalid prefix for a decimal number".into(),
326                action: "0 will be used instead",
327                pltotf_message: "An \"R\" or \"D\" value is needed here".into(),
328                pltotf_section: (62,1),
329            },
330            HeaderIndexIsTooSmall => Data {
331                rule: "header indices must be at least 18".into(),
332                problem: "header index is too small".into(),
333                action: "this property list element will be ignored",
334                pltotf_message: "HEADER indices should be 18 or more".into(),
335                pltotf_section: (91,1),
336            },
337            DesignSizeIsTooSmall => Data {
338                rule: "the design size must be at least 1".into(),
339                problem: "the design size is too small".into(),
340                action: "this property list element will be ignored",
341                pltotf_message: "The design size must be at least 1".into(),
342                pltotf_section: (88, 1),
343            },
344            _ => todo!("unhandled {self:?}"),
345        }
346    }
347}
348
349impl ParseWarning {
350    #[cfg(feature = "ariadne")]
351    pub fn ariadne_report<'a>(
352        &self,
353        file_name: &'a str,
354    ) -> ariadne::Report<'a, (&'a str, std::ops::Range<usize>)> {
355        use ariadne::*;
356        let light_blue = Color::Fixed(81);
357
358        let data = self.kind.data();
359        let mut builder = Report::build(ReportKind::Error, (file_name, self.span.clone()))
360            .with_code(format![
361                "{}.{}",
362                data.pltotf_section.0, data.pltotf_section.1,
363            ])
364            .with_message(&data.problem)
365            .with_label(
366                Label::new((file_name, self.span.clone()))
367                    .with_message(&data.problem)
368                    .with_color(light_blue),
369            )
370            .with_note(data.action);
371        if !data.rule.is_empty() {
372            builder = builder.with_help(data.rule);
373        }
374        builder.finish()
375    }
376}
377
378fn add_pltotf_error_context(pl_source: &str, error_message: String, error_point: usize) -> String {
379    let mut line_index = 0;
380    let mut line_offset = 0;
381    let mut total_chars = 0;
382    for (i, c) in super::Chars::new(pl_source).enumerate() {
383        total_chars += 1;
384        if error_point == i {
385            break;
386        }
387        line_offset += c.len_utf8();
388        if c == '\n' {
389            line_index += 1;
390            line_offset = 0;
391        }
392    }
393    if error_point == total_chars {
394        // This handles the unbalanced open parenthesis error.
395        let num_lines = line_index + 1;
396        return format!("{error_message} (line {num_lines}).\n...) \n    ...",);
397    }
398    let line = pl_source
399        .lines()
400        .nth(line_index)
401        .expect("we know there are line_index+1 lines in the file");
402    let start = &line[..line_offset];
403    let end = &line[line_offset..];
404    let line_number = line_index + 1;
405    format!(
406        "{error_message} (line {line_number}).\n{start} \n{}{end}  ",
407        " ".repeat(start.len())
408    )
409}