Skip to content

Commit

Permalink
[IFT] Add a custom error type for IFT patch application.
Browse files Browse the repository at this point in the history
  • Loading branch information
garretrieger committed Oct 7, 2024
1 parent bb12905 commit 7a6e0c4
Showing 1 changed file with 80 additions and 35 deletions.
115 changes: 80 additions & 35 deletions incremental-font-transfer/src/font_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://w3c.github.io/IFT/Overview.html#font-patch-formats>
Expand All @@ -35,19 +33,20 @@ impl PatchUri {
pub fn into_patch<'a>(
&self,
patch_data: &'a [u8],
) -> Result<IncrementalFontPatch<'a>, ReadError> {
) -> Result<IncrementalFontPatch<'a>, 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!()
}
};

if *self.expected_compatibility_id() != patch.compatibility_id() {
// Compatibility ids must match.
return Err(ReadError::ValidationError);
return Err(PatchingError::IncompatiblePatch);
}

Ok(patch)
Expand All @@ -64,17 +63,52 @@ 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<Vec<u8>, ReadError>;
fn apply_patch(&self, patch: IncrementalFontPatch) -> Result<Vec<u8>, 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 {
IncrementalFontPatch::TableKeyed(patch_data) => patch_data.compatibility_id(),
}
}

fn apply_to(&self, font: &FontRef) -> Result<Vec<u8>, ReadError> {
fn apply_to(&self, font: &FontRef) -> Result<Vec<u8>, PatchingError> {
match self {
IncrementalFontPatch::TableKeyed(patch_data) => {
apply_table_keyed_patch(patch_data, font)
Expand All @@ -84,34 +118,35 @@ impl IncrementalFontPatch<'_> {
}

impl IncrementalFontPatchBase for FontRef<'_> {
fn apply_patch(&self, patch: IncrementalFontPatch) -> Result<Vec<u8>, ReadError> {
fn apply_patch(&self, patch: IncrementalFontPatch) -> Result<Vec<u8>, 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)
}
}

impl IncrementalFontPatchBase for &[u8] {
fn apply_patch(&self, patch: IncrementalFontPatch) -> Result<Vec<u8>, ReadError> {
let font_ref = FontRef::new(self)?;
fn apply_patch(&self, patch: IncrementalFontPatch) -> Result<Vec<u8>, PatchingError> {
let font_ref = FontRef::new(self).map_err(PatchingError::FontParsingFailed)?;
font_ref.apply_patch(patch)
}
}

fn apply_table_keyed_patch(
patch: &TableKeyedPatch<'_>,
font: &FontRef,
) -> Result<Vec<u8>, ReadError> {
) -> Result<Vec<u8>, 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::<Tag>::new();
Expand All @@ -120,28 +155,30 @@ 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();
let next_offset = next_offset.get().to_u32();
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();
Expand Down Expand Up @@ -178,31 +215,37 @@ fn apply_table_patch(
table_patch: TablePatch,
stream_length: u32,
replacement: bool,
) -> Result<Vec<u8>, ReadError> {
) -> Result<Vec<u8>, 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(
stream,
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.")
}
})
}
Expand Down Expand Up @@ -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()
);
}
Expand All @@ -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)
);
}
Expand Down Expand Up @@ -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)
Expand All @@ -405,7 +448,7 @@ mod tests {
.unwrap();

assert_eq!(
Err(ReadError::OutOfBounds),
Err(PatchingError::PatchParsingFailed(ReadError::OutOfBounds)),
test_font().as_slice().apply_patch(patch,)
);
}
Expand Down Expand Up @@ -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),
);
}
Expand Down Expand Up @@ -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),
);
}
}

0 comments on commit 7a6e0c4

Please sign in to comment.