From 7a6e0c4393c88b5edb2af13b0025571715117a5e Mon Sep 17 00:00:00 2001 From: Garret Rieger Date: Mon, 7 Oct 2024 22:34:00 +0000 Subject: [PATCH] [IFT] Add a custom error type for IFT patch application. --- incremental-font-transfer/src/font_patch.rs | 115 ++++++++++++++------ 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/incremental-font-transfer/src/font_patch.rs b/incremental-font-transfer/src/font_patch.rs index 3187423a..50ad98a5 100644 --- a/incremental-font-transfer/src/font_patch.rs +++ b/incremental-font-transfer/src/font_patch.rs @@ -21,8 +21,6 @@ use shared_brotli_patch_decoder::{shared_brotli_decode, DecodeError}; use std::collections::BTreeSet; use write_fonts::FontBuilder; -// TODO(garretrieger): introduce a custom error type. - /// An incremental font patch which can be used to extend a font. /// /// See: @@ -35,11 +33,12 @@ impl PatchUri { pub fn into_patch<'a>( &self, patch_data: &'a [u8], - ) -> Result, ReadError> { + ) -> Result, PatchingError> { let patch = match self.encoding() { - PatchEncoding::TableKeyed { .. } => { - IncrementalFontPatch::TableKeyed(TableKeyedPatch::read(FontData::new(patch_data))?) - } + PatchEncoding::TableKeyed { .. } => IncrementalFontPatch::TableKeyed( + TableKeyedPatch::read(FontData::new(patch_data)) + .map_err(PatchingError::PatchParsingFailed)?, + ), PatchEncoding::GlyphKeyed => { todo!() } @@ -47,7 +46,7 @@ impl PatchUri { if *self.expected_compatibility_id() != patch.compatibility_id() { // Compatibility ids must match. - return Err(ReadError::ValidationError); + return Err(PatchingError::IncompatiblePatch); } Ok(patch) @@ -64,9 +63,44 @@ pub trait IncrementalFontPatchBase { /// expected_compatibility_id and has the specified encoding. /// /// Returns the byte data for the new font produced as a result of the patch application. - fn apply_patch(&self, patch: IncrementalFontPatch) -> Result, ReadError>; + fn apply_patch(&self, patch: IncrementalFontPatch) -> Result, PatchingError>; +} + +/// An error that occurs while trying to apply an IFT patch to a font file. +#[derive(Debug, Clone, PartialEq)] +pub enum PatchingError { + PatchParsingFailed(ReadError), + FontParsingFailed(ReadError), + IncompatiblePatch, + NonIncrementalFont, + InvalidPatch(&'static str), +} + +impl std::fmt::Display for PatchingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + PatchingError::PatchParsingFailed(err) => { + write!(f, "Failed to parse patch file: {}", err) + } + PatchingError::FontParsingFailed(err) => { + write!(f, "Failed to parse font file: {}", err) + } + PatchingError::IncompatiblePatch => { + write!(f, "Compatibility ID of the patch does not match the font.") + } + PatchingError::NonIncrementalFont => { + write!( + f, + "Can't patch font as it's not an incremental transfer font." + ) + } + PatchingError::InvalidPatch(msg) => write!(f, "Invalid patch file: '{msg}'"), + } + } } +impl std::error::Error for PatchingError {} + impl IncrementalFontPatch<'_> { fn compatibility_id(&self) -> CompatibilityId { match self { @@ -74,7 +108,7 @@ impl IncrementalFontPatch<'_> { } } - fn apply_to(&self, font: &FontRef) -> Result, ReadError> { + fn apply_to(&self, font: &FontRef) -> Result, PatchingError> { match self { IncrementalFontPatch::TableKeyed(patch_data) => { apply_table_keyed_patch(patch_data, font) @@ -84,13 +118,13 @@ impl IncrementalFontPatch<'_> { } impl IncrementalFontPatchBase for FontRef<'_> { - fn apply_patch(&self, patch: IncrementalFontPatch) -> Result, ReadError> { + fn apply_patch(&self, patch: IncrementalFontPatch) -> Result, PatchingError> { if self.table_data(Tag::new(b"IFT ")).is_none() && self.table_data(Tag::new(b"IFTX")).is_none() { // This base is not an incremental font, which is an error. // See: https://w3c.github.io/IFT/Overview.html#apply-table-keyed - return Err(ReadError::ValidationError); + return Err(PatchingError::NonIncrementalFont); } patch.apply_to(self) @@ -98,8 +132,8 @@ impl IncrementalFontPatchBase for FontRef<'_> { } impl IncrementalFontPatchBase for &[u8] { - fn apply_patch(&self, patch: IncrementalFontPatch) -> Result, ReadError> { - let font_ref = FontRef::new(self)?; + fn apply_patch(&self, patch: IncrementalFontPatch) -> Result, PatchingError> { + let font_ref = FontRef::new(self).map_err(PatchingError::FontParsingFailed)?; font_ref.apply_patch(patch) } } @@ -107,11 +141,12 @@ impl IncrementalFontPatchBase for &[u8] { fn apply_table_keyed_patch( patch: &TableKeyedPatch<'_>, font: &FontRef, -) -> Result, ReadError> { +) -> Result, PatchingError> { if patch.format() != Tag::new(b"iftk") { - return Err(ReadError::ValidationError); + return Err(PatchingError::InvalidPatch("Patch file tag is not 'iftk'")); } + // brotli stream starts at the (u32 tag + u8 flags + u32 length) = 9th byte const STREAM_START: u32 = 9; let mut font_builder = FontBuilder::new(); let mut processed_tables = BTreeSet::::new(); @@ -120,12 +155,15 @@ fn apply_table_keyed_patch( let i = i as usize; let next = i + 1; - let table_patch = patch.patches().get(i)?; + let table_patch = patch + .patches() + .get(i) + .map_err(PatchingError::PatchParsingFailed)?; let (Some(offset), Some(next_offset)) = ( patch.patch_offsets().get(i), patch.patch_offsets().get(next), ) else { - return Err(ReadError::MalformedData("Missing patch offset.")); + return Err(PatchingError::InvalidPatch("Missing patch offset.")); }; let offset = offset.get().to_u32(); @@ -133,15 +171,14 @@ fn apply_table_keyed_patch( let Some(stream_length) = next_offset .checked_sub(offset) .and_then(|v| v.checked_sub(STREAM_START)) - // brotli stream starts at the (u32 tag + u8 flags + u32 length) = 9th byte else { - return Err(ReadError::MalformedData( + return Err(PatchingError::InvalidPatch( "Patch offsets are not in sorted order.", )); }; if stream_length as usize > table_patch.brotli_stream().len() { - return Err(ReadError::OutOfBounds); + return Err(PatchingError::PatchParsingFailed(ReadError::OutOfBounds)); } let tag = table_patch.tag(); @@ -178,13 +215,15 @@ fn apply_table_patch( table_patch: TablePatch, stream_length: u32, replacement: bool, -) -> Result, ReadError> { +) -> Result, PatchingError> { let stream_length = stream_length as usize; let base_data = font.table_data(table_patch.tag()); let stream = if table_patch.brotli_stream().len() >= stream_length { &table_patch.brotli_stream()[..stream_length] } else { - return Err(ReadError::OutOfBounds); + return Err(PatchingError::InvalidPatch( + "Brotli stream is larger then the maxUncompressedLength field.", + )); }; let r = match (base_data, replacement) { (Some(base_data), false) => shared_brotli_decode( @@ -192,17 +231,21 @@ fn apply_table_patch( Some(base_data.as_bytes()), table_patch.max_uncompressed_length() as usize, ), - (None, false) => return Err(ReadError::ValidationError), + (None, false) => { + return Err(PatchingError::InvalidPatch( + "Trying to patch a base table that doesn't exist.", + )) + } _ => shared_brotli_decode(stream, None, table_patch.max_uncompressed_length() as usize), }; r.map_err(|decode_error| match decode_error { - DecodeError::InitFailure => ReadError::MalformedData("Failure to init brotli encoder."), - DecodeError::InvalidStream => ReadError::MalformedData("Malformed brotli stream."), - DecodeError::InvalidDictionary => ReadError::MalformedData("Malformed dictionary."), - DecodeError::MaxSizeExceeded => ReadError::OutOfBounds, + DecodeError::InitFailure => PatchingError::InvalidPatch("Failure to init brotli encoder."), + DecodeError::InvalidStream => PatchingError::InvalidPatch("Malformed brotli stream."), + DecodeError::InvalidDictionary => PatchingError::InvalidPatch("Malformed dictionary."), + DecodeError::MaxSizeExceeded => PatchingError::InvalidPatch("Max size exceeded."), DecodeError::ExcessInputData => { - ReadError::MalformedData("Input brotli stream has excess bytes.") + PatchingError::InvalidPatch("Input brotli stream has excess bytes.") } }) } @@ -312,7 +355,7 @@ mod tests { ); let patch_data = table_keyed_patch(); assert_eq!( - ReadError::ValidationError, + PatchingError::IncompatiblePatch, uri.into_patch(patch_data.as_slice()).err().unwrap() ); } @@ -326,7 +369,7 @@ mod tests { .unwrap(); assert_eq!( - Err(ReadError::ValidationError), + Err(PatchingError::InvalidPatch("Patch file tag is not 'iftk'")), test_font().as_slice().apply_patch(patch) ); } @@ -386,7 +429,7 @@ mod tests { .unwrap(); assert_eq!( - Err(ReadError::MalformedData( + Err(PatchingError::InvalidPatch( "Patch offsets are not in sorted order." )), test_font().as_slice().apply_patch(patch) @@ -405,7 +448,7 @@ mod tests { .unwrap(); assert_eq!( - Err(ReadError::OutOfBounds), + Err(PatchingError::PatchParsingFailed(ReadError::OutOfBounds)), test_font().as_slice().apply_patch(patch,) ); } @@ -456,7 +499,9 @@ mod tests { .unwrap(); assert_eq!( - Err(ReadError::ValidationError), + Err(PatchingError::InvalidPatch( + "Trying to patch a base table that doesn't exist." + )), test_font().as_slice().apply_patch(patch), ); } @@ -546,8 +591,8 @@ mod tests { .unwrap(); assert_eq!( - Err(ReadError::OutOfBounds), - test_font().as_slice().apply_patch(patch,) + Err(PatchingError::InvalidPatch("Max size exceeded.")), + test_font().as_slice().apply_patch(patch), ); } }