use std::collections::HashMap;
use crate::patch_group::PatchInfo;
use crate::glyph_keyed::apply_glyph_keyed_patches;
use crate::table_keyed::apply_table_keyed_patch;
use font_types::Tag;
use read_fonts::tables::ift::{CompatibilityId, GlyphKeyedPatch, TableKeyedPatch};
use skera::serialize::SerializeErrorFlags;
use read_fonts::{FontData, FontRead, FontRef, ReadError};
use shared_brotli_patch_decoder::decode_error::DecodeError;
use shared_brotli_patch_decoder::SharedBrotliDecoder;
pub trait IncrementalFontPatchBase {
fn apply_table_keyed_patch<D: SharedBrotliDecoder>(
&self,
patch: &PatchInfo,
patch_data: &[u8],
brotli_decoder: &D,
) -> Result<Vec<u8>, PatchingError>;
fn apply_glyph_keyed_patches<'a, D: SharedBrotliDecoder>(
&self,
patches: impl Iterator<Item = (&'a PatchInfo, &'a [u8])>,
brotli_decoder: &D,
) -> Result<Vec<u8>, PatchingError>;
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatchingError {
PatchParsingFailed(ReadError),
FontParsingFailed(ReadError),
SerializationError(SerializeErrorFlags),
IncompatiblePatch,
NonIncrementalFont,
InvalidPatch(&'static str),
EmptyPatchList,
InternalError,
MissingPatches,
}
impl From<SerializeErrorFlags> for PatchingError {
fn from(err: SerializeErrorFlags) -> Self {
PatchingError::SerializationError(err)
}
}
impl From<ReadError> for PatchingError {
fn from(err: ReadError) -> Self {
PatchingError::FontParsingFailed(err)
}
}
impl From<DecodeError> for PatchingError {
fn from(decoding_error: DecodeError) -> Self {
match decoding_error {
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 => {
PatchingError::InvalidPatch("Input brotli stream has excess bytes.")
}
DecodeError::IoError(_) => {
PatchingError::InvalidPatch("IO error decoding input brotli stream.")
}
}
}
}
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::SerializationError(err) => {
write!(f, "serialization failure constructing patched table: {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}'"),
PatchingError::EmptyPatchList => write!(f, "At least one patch file must be provided."),
PatchingError::InternalError => write!(
f,
"Internal constraint violated, typically should not happen."
),
PatchingError::MissingPatches => write!(f, "Not all patch data has been supplied."),
}
}
}
impl std::error::Error for PatchingError {}
impl IncrementalFontPatchBase for FontRef<'_> {
fn apply_table_keyed_patch<D: SharedBrotliDecoder>(
&self,
patch: &PatchInfo,
patch_data: &[u8],
brotli_decoder: &D,
) -> Result<Vec<u8>, PatchingError> {
let font_compat_id = patch
.tag()
.font_compat_id(self)
.map_err(PatchingError::FontParsingFailed)?;
if font_compat_id != *patch.tag().expected_compat_id() {
return Err(PatchingError::IncompatiblePatch);
}
let patch = TableKeyedPatch::read(FontData::new(patch_data))
.map_err(PatchingError::PatchParsingFailed)?;
if patch.compatibility_id() != font_compat_id {
return Err(PatchingError::IncompatiblePatch);
}
apply_table_keyed_patch(&patch, self, brotli_decoder)
}
fn apply_glyph_keyed_patches<'a, D: SharedBrotliDecoder>(
&self,
patches: impl Iterator<Item = (&'a PatchInfo, &'a [u8])>,
brotli_decoder: &D,
) -> Result<Vec<u8>, PatchingError> {
let mut cached_compat_ids: HashMap<Tag, Result<CompatibilityId, PatchingError>> =
Default::default();
let mut raw_patches: Vec<(&PatchInfo, GlyphKeyedPatch<'_>)> = vec![];
for (patch_info, patch_data) in patches {
let tag = patch_info.tag();
let font_compat_id = cached_compat_ids
.entry(tag.tag())
.or_insert_with(|| {
tag.font_compat_id(self)
.map_err(PatchingError::FontParsingFailed)
})
.as_ref()
.map_err(Clone::clone)?;
if font_compat_id != tag.expected_compat_id() {
return Err(PatchingError::IncompatiblePatch);
}
let patch = GlyphKeyedPatch::read(FontData::new(patch_data))
.map_err(PatchingError::PatchParsingFailed)?;
if *font_compat_id != patch.compatibility_id() {
return Err(PatchingError::IncompatiblePatch);
}
raw_patches.push((patch_info, patch));
}
apply_glyph_keyed_patches(&raw_patches, self, brotli_decoder)
}
}
impl IncrementalFontPatchBase for &[u8] {
fn apply_table_keyed_patch<D: SharedBrotliDecoder>(
&self,
patch: &PatchInfo,
patch_data: &[u8],
brotli_decoder: &D,
) -> Result<Vec<u8>, PatchingError> {
FontRef::new(self)
.map_err(PatchingError::FontParsingFailed)?
.apply_table_keyed_patch(patch, patch_data, brotli_decoder)
}
fn apply_glyph_keyed_patches<'a, D: SharedBrotliDecoder>(
&self,
patches: impl Iterator<Item = (&'a PatchInfo, &'a [u8])>,
brotli_decoder: &D,
) -> Result<Vec<u8>, PatchingError> {
FontRef::new(self)
.map_err(PatchingError::FontParsingFailed)?
.apply_glyph_keyed_patches(patches, brotli_decoder)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use font_test_data::ift::{
codepoints_only_format2, glyf_u16_glyph_patches, glyph_keyed_patch_header,
table_keyed_patch,
};
use read_fonts::{
collections::IntSet,
tables::ift::{CompatibilityId, IFTX_TAG, IFT_TAG},
};
use shared_brotli_patch_decoder::BuiltInBrotliDecoder;
use crate::{
font_patch::PatchingError,
glyph_keyed::tests::assemble_glyph_keyed_patch,
patchmap::{IftTableTag, PatchId, PatchUrl},
testdata::test_font_for_patching_with_loca_mod,
};
use super::{IncrementalFontPatchBase, PatchInfo};
#[test]
fn table_keyed_patch_and_font_compat_id_mismatch() {
let info = PatchInfo {
url: PatchUrl::expand_template(
&[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
&PatchId::Numeric(0),
)
.unwrap(),
source_table: IftTableTag::Ift(CompatibilityId::from_u32s([1, 2, 3, 4])),
application_flag_bit_indices: IntSet::<u32>::empty(),
};
let ift_table = codepoints_only_format2();
let mut iftx_table = codepoints_only_format2();
iftx_table.write_at("compat_id[0]", 2u32);
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([
(IFT_TAG, ift_table.as_slice()),
(IFTX_TAG, iftx_table.as_slice()),
]),
);
let mut patch = table_keyed_patch();
patch.write_at("compat_id", 2);
assert_eq!(
font.as_slice()
.apply_table_keyed_patch(&info, &patch, &BuiltInBrotliDecoder),
Err(PatchingError::IncompatiblePatch)
);
}
#[test]
fn table_keyed_patch_info_and_font_compat_id_mismatch() {
let info = PatchInfo {
url: PatchUrl::expand_template(
&[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
&PatchId::Numeric(0),
)
.unwrap(),
source_table: IftTableTag::Ift(CompatibilityId::from_u32s([2, 2, 3, 4])),
application_flag_bit_indices: IntSet::<u32>::empty(),
};
let ift_table = codepoints_only_format2();
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([(IFT_TAG, ift_table.as_slice())]),
);
let patch = table_keyed_patch();
assert_eq!(
font.as_slice()
.apply_table_keyed_patch(&info, &patch, &BuiltInBrotliDecoder),
Err(PatchingError::IncompatiblePatch)
);
}
#[test]
fn glyph_keyed_patch_and_font_compat_id_mismatch() {
let info = PatchInfo {
url: PatchUrl::expand_template(
&[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
&PatchId::Numeric(0),
)
.unwrap(),
source_table: IftTableTag::Ift(CompatibilityId::from_u32s([1, 2, 3, 4])),
application_flag_bit_indices: IntSet::<u32>::empty(),
};
let ift_table = codepoints_only_format2();
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([(IFT_TAG, ift_table.as_slice())]),
);
let patch =
assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
let input = vec![(&info, patch.as_slice())];
assert_eq!(
font.as_slice()
.apply_glyph_keyed_patches(input.into_iter(), &BuiltInBrotliDecoder),
Err(PatchingError::IncompatiblePatch)
);
}
#[test]
fn glyph_keyed_patch_info_and_font_compat_id_mismatch() {
let info = PatchInfo {
url: PatchUrl::expand_template(
&[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
&PatchId::Numeric(0),
)
.unwrap(),
source_table: IftTableTag::Ift(CompatibilityId::from_u32s([6, 7, 9, 9])),
application_flag_bit_indices: IntSet::<u32>::empty(),
};
let mut ift_table = codepoints_only_format2();
ift_table.write_at("compat_id[0]", 6u32);
ift_table.write_at("compat_id[1]", 7u32);
ift_table.write_at("compat_id[2]", 8u32);
ift_table.write_at("compat_id[3]", 9u32);
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([(IFT_TAG, ift_table.as_slice())]),
);
let patch =
assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
let input = vec![(&info, patch.as_slice())];
assert_eq!(
font.as_slice()
.apply_glyph_keyed_patches(input.into_iter(), &BuiltInBrotliDecoder),
Err(PatchingError::IncompatiblePatch)
);
}
}