use read_fonts::{
collections::IntSet, tables::ift::CompatibilityId, FontRef, ReadError, TableProvider,
};
use shared_brotli_patch_decoder::{BuiltInBrotliDecoder, SharedBrotliDecoder};
use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet, HashMap},
};
use crate::{
font_patch::{IncrementalFontPatchBase, PatchingError},
patchmap::{
intersecting_patches, IftTableTag, IntersectionInfo, PatchFormat, PatchMapEntry, PatchUrl,
SubsetDefinition,
},
};
pub struct PatchGroup<'a> {
font: FontRef<'a>,
patches: Option<CompatibleGroup>,
preload_urls: BTreeSet<PatchUrl>,
}
enum Selection {
IftPartial,
IftxPartial,
IftNo,
IftxNo,
Done,
}
impl Selection {
fn tag_index(&self) -> usize {
match self {
Self::IftPartial | Self::IftNo => 0,
_ => 1,
}
}
fn next(self) -> Selection {
match self {
Self::IftPartial => Self::IftxPartial,
Self::IftxPartial => Self::IftNo,
Self::IftNo => Self::IftxNo,
Self::IftxNo => Self::Done,
Self::Done => Self::Done,
}
}
}
impl PatchGroup<'_> {
pub fn select_next_patches<'b>(
ift_font: FontRef<'b>,
patch_data: &HashMap<PatchUrl, UrlStatus>,
subset_definition: &SubsetDefinition,
) -> Result<PatchGroup<'b>, ReadError> {
let candidates = intersecting_patches(&ift_font, subset_definition)?;
if candidates.is_empty() {
return Ok(PatchGroup {
font: ift_font,
patches: None,
preload_urls: Default::default(),
});
}
let ift_compat_id = ift_font.ift().ok().map(|t| t.compatibility_id());
let iftx_compat_id = ift_font.iftx().ok().map(|t| t.compatibility_id());
if ift_compat_id == iftx_compat_id {
return Err(ReadError::ValidationError);
}
let (compat_group, preload_urls) = Self::select_next_patches_from_candidates(
candidates,
patch_data,
ift_compat_id,
iftx_compat_id,
)?;
Ok(PatchGroup {
font: ift_font,
patches: Some(compat_group),
preload_urls,
})
}
pub fn urls(&self) -> impl Iterator<Item = &PatchUrl> {
self.invalidating_patch_iter()
.chain(self.non_invalidating_patch_iter())
.map(|info| &info.url)
.chain(self.preload_urls.iter())
}
pub fn has_urls(&self) -> bool {
let Some(patches) = &self.patches else {
return !self.preload_urls.is_empty();
};
match patches {
CompatibleGroup::Full(FullInvalidationPatch(_)) => true,
CompatibleGroup::Mixed { ift, iftx } => {
ift.has_urls() || iftx.has_urls() || !self.preload_urls.is_empty()
}
}
}
pub fn next_invalidating_patch(&self) -> Option<&PatchInfo> {
self.invalidating_patch_iter().next()
}
fn invalidating_patch_iter(&self) -> impl Iterator<Item = &PatchInfo> {
let full = match &self.patches {
Some(CompatibleGroup::Full(info)) => Some(&info.0),
_ => None,
};
let partial_1 = match &self.patches {
Some(CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(v),
iftx: _,
}) => Some(&v.0),
_ => None,
};
let partial_2 = match &self.patches {
Some(CompatibleGroup::Mixed {
ift: _,
iftx: ScopedGroup::PartialInvalidation(v),
}) => Some(&v.0),
_ => None,
};
full.into_iter().chain(partial_1).chain(partial_2)
}
fn non_invalidating_patch_iter(&self) -> impl Iterator<Item = &PatchInfo> {
let ift = match &self.patches {
Some(CompatibleGroup::Mixed { ift, iftx: _ }) => Some(ift),
_ => None,
};
let iftx = match &self.patches {
Some(CompatibleGroup::Mixed { ift: _, iftx }) => Some(iftx),
_ => None,
};
let it1 = ift
.into_iter()
.flat_map(|scope| scope.no_invalidation_iter());
let it2 = iftx
.into_iter()
.flat_map(|scope| scope.no_invalidation_iter());
it1.chain(it2)
}
fn select_next_patches_from_candidates(
candidates: Vec<PatchMapEntry>,
patch_data: &HashMap<PatchUrl, UrlStatus>,
ift_compat_id: Option<CompatibilityId>,
iftx_compat_id: Option<CompatibilityId>,
) -> Result<(CompatibleGroup, BTreeSet<PatchUrl>), ReadError> {
let GroupingByInvalidation {
full_invalidation,
partial_invalidation_ift,
partial_invalidation_iftx,
no_invalidation_ift,
no_invalidation_iftx,
} = GroupingByInvalidation::group_patches(
candidates,
patch_data,
ift_compat_id,
iftx_compat_id,
);
let mut combined_preload_urls: BTreeSet<PatchUrl> = Default::default();
if let Some(patch) = Self::select_invalidating_candidate(full_invalidation) {
combined_preload_urls = patch.preload_urls.iter().cloned().collect();
combined_preload_urls.remove(&patch.patch_info.url);
return Ok((CompatibleGroup::Full(patch.into()), combined_preload_urls));
}
let mut selection_mode = Selection::IftPartial;
let mut scoped_groups: [Option<ScopedGroup>; 2] = [None, None];
let mut partial_invalidation_candidates =
[partial_invalidation_ift, partial_invalidation_iftx];
let mut no_invalidation_candidates = [no_invalidation_ift, no_invalidation_iftx];
let mut selected_urls: BTreeSet<PatchUrl> = Default::default();
loop {
let tag_index = selection_mode.tag_index();
match selection_mode {
Selection::IftPartial | Selection::IftxPartial => {
let mut candidates: Vec<CandidatePatch> = vec![];
std::mem::swap(
&mut candidates,
&mut partial_invalidation_candidates[tag_index],
);
scoped_groups[tag_index] = Self::select_invalidating_candidate(
candidates
.into_iter()
.filter(|c| !selected_urls.contains(&c.patch_info.url)),
)
.map(|patch| {
combined_preload_urls.extend(patch.preload_urls.iter().cloned());
selected_urls.insert(patch.patch_info.url.clone());
ScopedGroup::PartialInvalidation(patch.into())
})
}
Selection::IftNo | Selection::IftxNo => {
if scoped_groups[tag_index].is_none() {
let mut candidates: BTreeMap<PatchUrl, CandidateNoInvalidationPatch> =
Default::default();
std::mem::swap(&mut candidates, &mut no_invalidation_candidates[tag_index]);
scoped_groups[tag_index] = Some(ScopedGroup::NoInvalidation(
Self::filter_and_extract_preloads(
candidates,
&mut selected_urls,
&mut combined_preload_urls,
),
))
}
}
Selection::Done => break,
};
selection_mode = selection_mode.next();
}
combined_preload_urls.retain(|url| !selected_urls.contains(url));
let [Some(ift), Some(iftx)] = scoped_groups else {
return Err(ReadError::MalformedData(
"Failed invariant. Both arms of the mixed compat group should always be filled.",
));
};
Ok((CompatibleGroup::Mixed { ift, iftx }, combined_preload_urls))
}
fn filter_and_extract_preloads(
candidates: BTreeMap<PatchUrl, CandidateNoInvalidationPatch>,
previously_selected_urls: &mut BTreeSet<PatchUrl>,
preloads: &mut BTreeSet<PatchUrl>,
) -> BTreeMap<OrderedPatchUrl, NoInvalidationPatch> {
preloads.extend(
candidates
.values()
.flat_map(|candidate| candidate.preload_urls.iter().cloned()),
);
let filtered: BTreeMap<OrderedPatchUrl, NoInvalidationPatch> = candidates
.into_iter()
.filter(|(k, _)| !previously_selected_urls.contains(k))
.map(|(k, v)| {
(
OrderedPatchUrl(v.entry_order, k),
NoInvalidationPatch(v.patch_info),
)
})
.collect();
for (url, _) in filtered.iter() {
previously_selected_urls.insert(url.1.clone());
}
filtered
}
fn select_invalidating_candidate<T>(candidates: T) -> Option<CandidatePatch>
where
T: IntoIterator<Item = CandidatePatch>,
{
candidates.into_iter().max()
}
pub fn apply_next_patches(
self,
patch_data: &mut HashMap<PatchUrl, UrlStatus>,
) -> Result<Vec<u8>, PatchingError> {
self.apply_next_patches_with_decoder(patch_data, &BuiltInBrotliDecoder)
}
pub fn apply_next_patches_with_decoder<D: SharedBrotliDecoder>(
self,
patch_data: &mut HashMap<PatchUrl, UrlStatus>,
brotli_decoder: &D,
) -> Result<Vec<u8>, PatchingError> {
if let Some(patch) = self.next_invalidating_patch() {
let entry = patch_data
.get_mut(&patch.url)
.ok_or(PatchingError::MissingPatches)?;
match entry {
UrlStatus::Pending(patch_data) => {
let r = self
.font
.apply_table_keyed_patch(patch, patch_data, brotli_decoder)?;
*entry = UrlStatus::Applied;
return Ok(r);
}
UrlStatus::Applied => {} }
}
let new_font = {
let mut accumulated_info: Vec<(&PatchInfo, &[u8])> = vec![];
for info in self.non_invalidating_patch_iter() {
let data = patch_data
.get(&info.url)
.ok_or(PatchingError::MissingPatches)?;
match data {
UrlStatus::Pending(data) => accumulated_info.push((info, data)),
UrlStatus::Applied => {} }
}
if accumulated_info.is_empty() {
return Err(PatchingError::EmptyPatchList);
}
self.font
.apply_glyph_keyed_patches(accumulated_info.into_iter(), brotli_decoder)?
};
for info in self.non_invalidating_patch_iter() {
if let Some(status) = patch_data.get_mut(&info.url) {
*status = UrlStatus::Applied;
};
}
Ok(new_font)
}
}
#[derive(Default)]
struct GroupingByInvalidation {
full_invalidation: Vec<CandidatePatch>,
partial_invalidation_ift: Vec<CandidatePatch>,
partial_invalidation_iftx: Vec<CandidatePatch>,
no_invalidation_ift: BTreeMap<PatchUrl, CandidateNoInvalidationPatch>,
no_invalidation_iftx: BTreeMap<PatchUrl, CandidateNoInvalidationPatch>,
}
impl GroupingByInvalidation {
fn group_patches(
candidates: Vec<PatchMapEntry>,
patch_data: &HashMap<PatchUrl, UrlStatus>,
ift_compat_id: Option<CompatibilityId>,
iftx_compat_id: Option<CompatibilityId>,
) -> GroupingByInvalidation {
let mut result: GroupingByInvalidation = Default::default();
for entry in candidates.into_iter() {
match entry.format {
PatchFormat::TableKeyed {
fully_invalidating: true,
} => result
.full_invalidation
.push(CandidatePatch::from_entry(entry, patch_data)),
PatchFormat::TableKeyed {
fully_invalidating: false,
} => {
if Some(entry.expected_compat_id()) == ift_compat_id.as_ref() {
result
.partial_invalidation_ift
.push(CandidatePatch::from_entry(entry, patch_data))
} else if Some(entry.expected_compat_id()) == iftx_compat_id.as_ref() {
result
.partial_invalidation_iftx
.push(CandidatePatch::from_entry(entry, patch_data))
}
}
PatchFormat::GlyphKeyed => {
let mapping = if Some(entry.expected_compat_id()) == ift_compat_id.as_ref() {
Some(&mut result.no_invalidation_ift)
} else if Some(entry.expected_compat_id()) == iftx_compat_id.as_ref() {
Some(&mut result.no_invalidation_iftx)
} else {
None
};
if let Some(mapping) = mapping {
mapping
.entry(entry.url().clone())
.and_modify(|existing| {
if entry.intersection_info.entry_order() < existing.entry_order {
*existing = entry.clone().into()
}
})
.or_insert_with(|| entry.into());
}
}
}
}
result
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum UrlStatus {
Applied,
Pending(Vec<u8>),
}
#[derive(PartialEq, Eq, Debug)]
pub struct PatchInfo {
pub(crate) url: PatchUrl,
pub(crate) source_table: IftTableTag,
pub(crate) application_flag_bit_indices: IntSet<u32>,
}
impl From<PatchMapEntry> for PatchInfo {
fn from(value: PatchMapEntry) -> Self {
PatchInfo {
url: value.url,
source_table: value.source_table,
application_flag_bit_indices: value.application_bit_indices,
}
}
}
impl PatchInfo {
pub(crate) fn tag(&self) -> &IftTableTag {
&self.source_table
}
pub(crate) fn application_flag_bit_indices(&self) -> impl Iterator<Item = u32> + '_ {
self.application_flag_bit_indices.iter()
}
pub fn url(&self) -> &str {
self.url.as_ref()
}
}
#[derive(PartialEq, Eq)]
struct CandidatePatch {
intersection_info: IntersectionInfo,
patch_info: PatchInfo,
preload_urls: Vec<PatchUrl>,
already_loaded: bool,
}
struct CandidateNoInvalidationPatch {
patch_info: PatchInfo,
entry_order: usize,
preload_urls: Vec<PatchUrl>,
}
impl CandidatePatch {
fn from_entry(
value: PatchMapEntry,
patch_data: &HashMap<PatchUrl, UrlStatus>,
) -> CandidatePatch {
let patch_info = PatchInfo {
url: value.url,
source_table: value.source_table,
application_flag_bit_indices: value.application_bit_indices,
};
let already_loaded = patch_data.contains_key(&patch_info.url);
Self {
intersection_info: value.intersection_info,
patch_info,
preload_urls: value.preload_urls,
already_loaded,
}
}
}
impl PartialOrd for CandidatePatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CandidatePatch {
fn cmp(&self, other: &Self) -> Ordering {
match (self.already_loaded, other.already_loaded) {
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
_ => self.intersection_info.cmp(&other.intersection_info),
}
}
}
impl From<PatchMapEntry> for CandidateNoInvalidationPatch {
fn from(mut value: PatchMapEntry) -> Self {
let mut preload_urls: Vec<PatchUrl> = vec![];
std::mem::swap(&mut preload_urls, &mut value.preload_urls);
let entry_order = value.intersection_info.entry_order();
Self {
patch_info: value.into(),
entry_order,
preload_urls,
}
}
}
#[derive(PartialEq, Eq, Debug)]
struct NoInvalidationPatch(PatchInfo);
#[derive(PartialEq, Eq, Debug)]
struct PartialInvalidationPatch(PatchInfo);
impl From<CandidatePatch> for PartialInvalidationPatch {
fn from(value: CandidatePatch) -> Self {
Self(value.patch_info)
}
}
#[derive(PartialEq, Eq, Debug)]
struct FullInvalidationPatch(PatchInfo);
impl From<CandidatePatch> for FullInvalidationPatch {
fn from(value: CandidatePatch) -> Self {
Self(value.patch_info)
}
}
#[derive(PartialEq, Eq, Debug)]
enum CompatibleGroup {
Full(FullInvalidationPatch),
Mixed { ift: ScopedGroup, iftx: ScopedGroup },
}
#[derive(PartialEq, Eq, Debug)]
enum ScopedGroup {
PartialInvalidation(PartialInvalidationPatch),
NoInvalidation(BTreeMap<OrderedPatchUrl, NoInvalidationPatch>),
}
impl ScopedGroup {
fn has_urls(&self) -> bool {
match self {
ScopedGroup::PartialInvalidation(PartialInvalidationPatch(_)) => true,
ScopedGroup::NoInvalidation(url_map) => !url_map.is_empty(),
}
}
fn no_invalidation_iter(&self) -> impl Iterator<Item = &PatchInfo> {
match self {
ScopedGroup::PartialInvalidation(_) => NoInvalidationPatchesIter { it: None },
ScopedGroup::NoInvalidation(map) => NoInvalidationPatchesIter {
it: Some(map.values()),
},
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct OrderedPatchUrl(usize, PatchUrl);
struct NoInvalidationPatchesIter<'a, T>
where
T: Iterator<Item = &'a NoInvalidationPatch>,
{
it: Option<T>,
}
impl<'a, T> Iterator for NoInvalidationPatchesIter<'a, T>
where
T: Iterator<Item = &'a NoInvalidationPatch>,
{
type Item = &'a PatchInfo;
fn next(&mut self) -> Option<Self::Item> {
let it = self.it.as_mut()?;
Some(&it.next()?.0)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::{
glyph_keyed::tests::assemble_glyph_keyed_patch, patchmap::PatchId,
testdata::test_font_for_patching_with_loca_mod,
};
use font_test_data::{
bebuffer::BeBuffer,
ift::{
custom_ids_format2, glyf_u16_glyph_patches, glyph_keyed_patch_header,
table_keyed_format2, table_keyed_patch, ABSOLUTE_URL_TEMPLATE, RELATIVE_URL_TEMPLATE,
},
};
use font_types::{Int24, Tag, Uint24};
use read_fonts::{
tables::ift::{IFTX_TAG, IFT_TAG},
FontRef,
};
use write_fonts::FontBuilder;
const TABLE_1_FINAL_STATE: &[u8] = "hijkabcdeflmnohijkabcdeflmno\n".as_bytes();
const TABLE_2_FINAL_STATE: &[u8] = "foobarbaz foobarbaz foobarbaz\n".as_bytes();
impl PatchUrl {
fn new(url: &str) -> Self {
Self(url.to_string())
}
}
impl OrderedPatchUrl {
fn url(order: usize, url: &str) -> Self {
OrderedPatchUrl(order, PatchUrl(url.to_string()))
}
}
fn base_font(ift: Option<BeBuffer>, iftx: Option<BeBuffer>) -> Vec<u8> {
let mut font_builder = FontBuilder::new();
if let Some(buffer) = &ift {
font_builder.add_raw(IFT_TAG, buffer.as_slice());
}
if let Some(buffer) = &iftx {
font_builder.add_raw(IFTX_TAG, buffer.as_slice());
}
font_builder.add_raw(Tag::new(b"tab1"), "abcdef\n".as_bytes());
font_builder.add_raw(Tag::new(b"tab2"), "foobar\n".as_bytes());
font_builder.add_raw(Tag::new(b"tab4"), "abcdef\n".as_bytes());
font_builder.add_raw(Tag::new(b"tab5"), "foobar\n".as_bytes());
font_builder.build()
}
fn cid_1() -> CompatibilityId {
CompatibilityId::from_u32s([0, 0, 0, 1])
}
fn cid_2() -> CompatibilityId {
CompatibilityId::from_u32s([0, 0, 0, 2])
}
fn p(index: u32, table: IftTableTag, format: PatchFormat) -> PatchMapEntry {
let url =
PatchUrl::expand_template(ABSOLUTE_URL_TEMPLATE, &PatchId::Numeric(index)).unwrap();
let mut e = url.into_format_1_entry(table, format, Default::default());
e.application_bit_indices.insert(42);
e
}
fn p1_full() -> PatchMapEntry {
p(
1,
IftTableTag::Ift(cid_1()),
PatchFormat::TableKeyed {
fully_invalidating: true,
},
)
}
fn p2_partial_c1() -> PatchMapEntry {
p(
2,
IftTableTag::Ift(cid_1()),
PatchFormat::TableKeyed {
fully_invalidating: false,
},
)
}
fn p2_partial_c2() -> PatchMapEntry {
p(
2,
IftTableTag::Iftx(cid_2()),
PatchFormat::TableKeyed {
fully_invalidating: false,
},
)
}
fn p2_no_c2() -> PatchMapEntry {
p(2, IftTableTag::Iftx(cid_2()), PatchFormat::GlyphKeyed)
}
fn p3_partial_c2() -> PatchMapEntry {
p(
3,
IftTableTag::Iftx(cid_2()),
PatchFormat::TableKeyed {
fully_invalidating: false,
},
)
}
fn p3_no_c1() -> PatchMapEntry {
p(3, IftTableTag::Ift(cid_1()), PatchFormat::GlyphKeyed)
}
fn p4_no_c1() -> PatchMapEntry {
p(4, IftTableTag::Ift(cid_1()), PatchFormat::GlyphKeyed)
}
fn p4_no_c2() -> PatchMapEntry {
p(4, IftTableTag::Iftx(cid_2()), PatchFormat::GlyphKeyed)
}
fn p5_no_c2() -> PatchMapEntry {
p(5, IftTableTag::Iftx(cid_2()), PatchFormat::GlyphKeyed)
}
fn full(index: u32, codepoints: u64) -> PatchMapEntry {
let url =
PatchUrl::expand_template(ABSOLUTE_URL_TEMPLATE, &PatchId::Numeric(index)).unwrap();
let mut e = url.into_format_1_entry(
IftTableTag::Ift(cid_1()),
PatchFormat::TableKeyed {
fully_invalidating: true,
},
IntersectionInfo::new(codepoints, 0, 0),
);
e.application_bit_indices.insert(42);
e
}
fn partial(index: u32, compat_id: CompatibilityId, codepoints: u64) -> PatchMapEntry {
let tag = if compat_id == cid_1() {
IftTableTag::Ift(compat_id)
} else {
IftTableTag::Iftx(compat_id)
};
let url =
PatchUrl::expand_template(ABSOLUTE_URL_TEMPLATE, &PatchId::Numeric(index)).unwrap();
let mut e = url.into_format_1_entry(
tag,
PatchFormat::TableKeyed {
fully_invalidating: false,
},
IntersectionInfo::new(codepoints, 0, 0),
);
e.application_bit_indices.insert(42);
e
}
fn patch_info_ift(url: &str) -> PatchInfo {
let mut application_flag_bit_indices = IntSet::<u32>::empty();
application_flag_bit_indices.insert(42);
PatchInfo {
url: PatchUrl::new(url),
application_flag_bit_indices,
source_table: IftTableTag::Ift(cid_1()),
}
}
fn patch_info_iftx(url: &str) -> PatchInfo {
let mut application_flag_bit_indices = IntSet::<u32>::empty();
application_flag_bit_indices.insert(42);
PatchInfo {
url: PatchUrl::new(url),
application_flag_bit_indices,
source_table: IftTableTag::Iftx(cid_2()),
}
}
#[test]
fn full_invalidation() {
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p1_full()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04")))
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![
p1_full(),
p2_partial_c1(),
p3_partial_c2(),
p4_no_c1(),
p5_no_c2(),
],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04"),))
);
assert!(preloads.is_empty());
}
fn preload_list(urls: &[String]) -> Vec<PatchUrl> {
urls.iter().map(|url| PatchUrl::new(url)).collect()
}
#[test]
fn full_invalidation_with_preloads() {
let expected_preloads = [PatchUrl::new("abc"), PatchUrl::new("def")];
let mut p1_full = p1_full();
p1_full.preload_urls.extend(expected_preloads.clone());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p1_full.clone()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04")))
);
assert_eq!(preloads, BTreeSet::from(expected_preloads.clone()));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![
p1_full,
p2_partial_c1(),
p3_partial_c2(),
p4_no_c1(),
p5_no_c2(),
],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04"),))
);
assert_eq!(preloads, BTreeSet::from(expected_preloads));
}
#[test]
fn full_invalidation_with_preloads_removes_duplicate_urls() {
let expected_preloads = [PatchUrl::new("abc"), PatchUrl::new("def")];
let mut p1_full = p1_full();
p1_full.preload_urls.extend(expected_preloads.clone());
p1_full
.preload_urls
.extend(preload_list(&["//foo.bar/04".to_string()]));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p1_full.clone()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04")))
);
assert_eq!(preloads, BTreeSet::from(expected_preloads.clone()));
}
#[test]
fn full_invalidation_selection_order() {
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![full(3, 9), full(1, 7), full(2, 24)],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/08")))
);
assert!(preloads.is_empty());
}
#[test]
fn partial_invalidation_selection_order() {
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![
partial(3, cid_1(), 9),
partial(1, cid_1(), 23),
partial(2, cid_1(), 24),
],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
),)),
iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![
partial(4, cid_2(), 1),
partial(5, cid_2(), 22),
partial(6, cid_2(), 2),
],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::default()),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0K"
),)),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![
partial(3, cid_1(), 9),
partial(1, cid_1(), 23),
partial(2, cid_1(), 24),
partial(4, cid_2(), 1),
partial(5, cid_2(), 22),
partial(6, cid_2(), 2),
],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
),)),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0K"
),)),
}
);
assert!(preloads.is_empty());
}
#[test]
fn partial_invalidation_with_preloaded() {
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![
partial(3, cid_1(), 9),
partial(1, cid_1(), 23),
partial(2, cid_1(), 24),
],
&HashMap::from([(PatchUrl::new("//foo.bar/0C"), UrlStatus::Pending(vec![]))]),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/0C"
),)),
iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![
partial(3, cid_1(), 9),
partial(1, cid_1(), 23),
partial(2, cid_1(), 24),
],
&HashMap::from([
(PatchUrl::new("//foo.bar/04"), UrlStatus::Pending(vec![])),
(PatchUrl::new("//foo.bar/0C"), UrlStatus::Pending(vec![])),
]),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/04"
),)),
iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
}
);
assert!(preloads.is_empty());
}
#[test]
fn partial_invalidation_with_preloads() {
let mut partial_c1 = partial(3, cid_1(), 9);
let mut partial_c2 = partial(4, cid_2(), 9);
let expected_preloads_c1 = [PatchUrl::new("abc"), PatchUrl::new("def")];
partial_c1.preload_urls.extend(expected_preloads_c1.clone());
let expected_preloads_c2 = [PatchUrl::new("hij"), PatchUrl::new("def")];
partial_c2.preload_urls.extend(expected_preloads_c2.clone());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![partial_c1.clone()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/0C"
),)),
iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
}
);
assert_eq!(preloads, BTreeSet::from(expected_preloads_c1.clone()));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![partial_c2.clone()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::default()),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0G"
),)),
}
);
assert_eq!(preloads, BTreeSet::from(expected_preloads_c2.clone()));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![partial_c1, partial_c2],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/0C"
),)),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0G"
),)),
}
);
assert_eq!(
preloads,
BTreeSet::from([
PatchUrl::new("abc"),
PatchUrl::new("def"),
PatchUrl::new("hij")
])
);
}
#[test]
fn partial_invalidation_with_preloads_removes_duplicates() {
let mut partial_c1 = partial(3, cid_1(), 9);
let mut partial_c2 = partial(4, cid_2(), 9);
let expected_preloads_c1 = ["abc".to_string(), "def".to_string()];
partial_c1
.preload_urls
.extend(preload_list(&expected_preloads_c1));
partial_c1.preload_urls.extend(preload_list(&[
"//foo.bar/0C".to_string(),
"//foo.bar/0G".to_string(),
]));
let expected_preloads_c2 = ["hij".to_string(), "def".to_string()];
partial_c2
.preload_urls
.extend(preload_list(&expected_preloads_c2));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![partial_c1, partial_c2],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/0C"
),)),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0G"
),)),
}
);
assert_eq!(
preloads,
BTreeSet::from([
PatchUrl::new("abc"),
PatchUrl::new("def"),
PatchUrl::new("hij"),
])
);
}
#[test]
fn no_invalidation_with_preloads() {
let expected_preloads_c1 = ["abc".to_string(), "def".to_string()];
let expected_preloads_c2 = ["hij".to_string(), "def".to_string()];
let mut p4_no_c1 = p4_no_c1();
p4_no_c1
.preload_urls
.extend(preload_list(&expected_preloads_c1));
let mut p4_no_c2 = p4_no_c2();
p4_no_c2
.preload_urls
.extend(preload_list(&expected_preloads_c1));
let mut p5_no_c2 = p5_no_c2();
p5_no_c2
.preload_urls
.extend(preload_list(&expected_preloads_c2));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p4_no_c1, p5_no_c2.clone()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
)])),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0K"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
)]))
}
);
assert_eq!(
preloads,
BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
);
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p4_no_c2, p5_no_c2],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(Default::default()),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([
(
OrderedPatchUrl::url(0, "//foo.bar/0K"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
),
(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0G"))
)
]))
}
);
assert_eq!(
preloads,
BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
);
}
#[test]
fn no_invalidation_with_preloads_removes_duplicates() {
let expected_preloads_c1 = ["abc".to_string(), "def".to_string()];
let expected_preloads_c2 = ["hij".to_string(), "def".to_string()];
let mut p4_no_c1 = p4_no_c1();
p4_no_c1
.preload_urls
.extend(preload_list(&expected_preloads_c1));
let mut p5_no_c2 = p5_no_c2();
p5_no_c2
.preload_urls
.extend(preload_list(&expected_preloads_c2));
p5_no_c2.preload_urls.extend(preload_list(&[
"//foo.bar/0G".to_string(),
"//foo.bar/0K".to_string(),
]));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p4_no_c1, p5_no_c2.clone()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
)])),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0K"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
)]))
}
);
assert_eq!(
preloads,
BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
);
}
#[test]
fn mixed() {
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
),)),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0K"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
)]))
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p3_partial_c2(), p4_no_c1(), p5_no_c2()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
)])),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0C"
),))
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1(), p4_no_c1()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
),)),
iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p3_partial_c2(), p5_no_c2()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::default()),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0C"
),)),
}
);
assert!(preloads.is_empty());
}
#[test]
fn mixed_with_preloads() {
let mut p2_partial_c1 = p2_partial_c1();
p2_partial_c1
.preload_urls
.extend(preload_list(&["abc".to_string(), "def".to_string()]));
let mut p3_partial_c2 = p3_partial_c2();
p3_partial_c2
.preload_urls
.extend(preload_list(&["klm".to_string(), "nop".to_string()]));
let mut p4_no_c1 = p4_no_c1();
p4_no_c1
.preload_urls
.extend(preload_list(&["foo".to_string(), "bar".to_string()]));
let mut p5_no_c2 = p5_no_c2();
p5_no_c2
.preload_urls
.extend(preload_list(&["hij".to_string(), "def".to_string()]));
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1, p4_no_c1.clone(), p5_no_c2.clone()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
),)),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0K"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
)]))
}
);
assert_eq!(
preloads,
BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
);
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p3_partial_c2, p4_no_c1, p5_no_c2],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
)])),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0C"
),))
}
);
assert_eq!(
preloads,
BTreeSet::from(["klm", "nop", "foo", "bar",].map(PatchUrl::new))
);
}
#[test]
fn missing_compat_ids() {
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
&Default::default(),
None,
None,
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(Default::default()),
iftx: ScopedGroup::NoInvalidation(Default::default()),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
&Default::default(),
Some(cid_1()),
None,
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
),)),
iftx: ScopedGroup::NoInvalidation(Default::default()),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
&Default::default(),
None,
Some(cid_1()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(Default::default()),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
),)),
}
);
assert!(preloads.is_empty());
}
#[test]
fn dedups_urls() {
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p4_no_c1(), p4_no_c1()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
)])),
iftx: ScopedGroup::NoInvalidation(BTreeMap::new()),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p4_no_c1(), p4_no_c2(), p5_no_c2()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
)])),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0K"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
)])),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1(), p2_partial_c2(), p3_partial_c2()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
))),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0C"
))),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p2_partial_c1(), p2_no_c2(), p5_no_c2()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
"//foo.bar/08"
))),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0K"),
NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
)])),
}
);
assert!(preloads.is_empty());
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
vec![p3_partial_c2(), p3_no_c1(), p4_no_c1()],
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
assert_eq!(
group,
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "//foo.bar/0G"),
NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
)])),
iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
"//foo.bar/0C"
))),
}
);
assert!(preloads.is_empty());
}
fn create_group_for(urls: Vec<PatchMapEntry>) -> PatchGroup<'static> {
let data = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
urls,
&Default::default(),
Some(cid_1()),
Some(cid_2()),
)
.unwrap();
PatchGroup {
font: data,
patches: Some(group),
preload_urls: preloads,
}
}
fn empty_group() -> PatchGroup<'static> {
let data = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
PatchGroup {
font: data,
patches: None,
preload_urls: Default::default(),
}
}
#[test]
fn urls() {
let g = create_group_for(vec![]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
Vec::<&str>::default()
);
assert!(!g.has_urls());
let g = empty_group();
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
Vec::<&str>::default()
);
assert!(!g.has_urls());
let g = create_group_for(vec![p1_full()]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/04"],
);
assert!(g.has_urls());
let g = create_group_for(vec![p2_partial_c1(), p3_partial_c2()]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/08", "//foo.bar/0C"]
);
assert!(g.has_urls());
let g = create_group_for(vec![p2_partial_c1()]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/08",],
);
assert!(g.has_urls());
let g = create_group_for(vec![p3_partial_c2()]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/0C"],
);
assert!(g.has_urls());
let g = create_group_for(vec![p2_partial_c1(), p4_no_c2(), p5_no_c2()]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/08", "//foo.bar/0G", "//foo.bar/0K"],
);
assert!(g.has_urls());
let g = create_group_for(vec![p3_partial_c2(), p4_no_c1()]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/0C", "//foo.bar/0G"],
);
let g = create_group_for(vec![p4_no_c1(), p5_no_c2()]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/0G", "//foo.bar/0K"],
);
assert!(g.has_urls());
}
#[test]
fn urls_with_preloads() {
let mut p2_partial_c1 = p2_partial_c1();
p2_partial_c1
.preload_urls
.extend(preload_list(&["abc".to_string(), "def".to_string()]));
let mut p3_partial_c2 = p3_partial_c2();
p3_partial_c2
.preload_urls
.extend(preload_list(&["foo".to_string(), "bar".to_string()]));
let g = create_group_for(vec![p2_partial_c1, p3_partial_c2]);
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
vec!["//foo.bar/08", "//foo.bar/0C", "abc", "bar", "def", "foo"]
);
assert!(g.has_urls());
}
#[test]
fn select_next_patches_no_intersection() {
let font = base_font(Some(table_keyed_format2()), None);
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([55].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
assert!(!g.has_urls());
assert_eq!(
g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
Vec::<&str>::default()
);
assert_eq!(
g.apply_next_patches(&mut Default::default()),
Err(PatchingError::EmptyPatchList)
);
}
#[test]
fn apply_patches_full_invalidation() {
let font = base_font(Some(table_keyed_format2()), None);
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
assert!(g.has_urls());
let mut patch_data = HashMap::from([
(
PatchUrl::new("foo/04"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
),
(
PatchUrl::new("foo/bar"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
),
]);
let new_font = g.apply_next_patches(&mut patch_data).unwrap();
let new_font = FontRef::new(&new_font).unwrap();
assert_eq!(
new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
TABLE_1_FINAL_STATE,
);
assert_eq!(
new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
TABLE_2_FINAL_STATE,
);
assert_eq!(
patch_data,
HashMap::from([
(PatchUrl::new("foo/04"), UrlStatus::Applied,),
(
PatchUrl::new("foo/bar"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
),
])
)
}
struct CustomBrotliDecoder;
impl SharedBrotliDecoder for CustomBrotliDecoder {
fn decode(
&self,
_encoded: &[u8],
_shared_dictionary: Option<&[u8]>,
_max_uncompressed_length: usize,
) -> Result<Vec<u8>, shared_brotli_patch_decoder::decode_error::DecodeError> {
Ok(vec![1, 2, 3, 4, 5])
}
}
#[test]
fn apply_patches_full_invalidation_with_custom_brotli() {
let font = base_font(Some(table_keyed_format2()), None);
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
assert!(g.has_urls());
let mut patch_data = HashMap::from([
(
PatchUrl::new("foo/04"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
),
(
PatchUrl::new("foo/bar"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
),
]);
let new_font = g
.apply_next_patches_with_decoder(&mut patch_data, &CustomBrotliDecoder)
.unwrap();
let new_font = FontRef::new(&new_font).unwrap();
assert_eq!(
new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
vec![1, 2, 3, 4, 5]
);
assert_eq!(
new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
vec![1, 2, 3, 4, 5]
);
assert_eq!(
patch_data,
HashMap::from([
(PatchUrl::new("foo/04"), UrlStatus::Applied,),
(
PatchUrl::new("foo/bar"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
),
])
)
}
#[test]
fn apply_patches_one_partial_invalidation() {
let mut buffer = table_keyed_format2();
buffer.write_at("encoding", 2u8);
let font = base_font(Some(buffer.clone()), None);
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
let mut patch_data = HashMap::from([(
PatchUrl::new("foo/04"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
)]);
let new_font = g.apply_next_patches(&mut patch_data).unwrap();
let new_font = FontRef::new(&new_font).unwrap();
assert_eq!(
new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
TABLE_1_FINAL_STATE,
);
assert_eq!(
new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
TABLE_2_FINAL_STATE,
);
assert_eq!(
patch_data,
HashMap::from([(PatchUrl::new("foo/04"), UrlStatus::Applied,),])
);
let font = base_font(None, Some(buffer.clone()));
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
let mut patch_data = HashMap::from([(
PatchUrl::new("foo/04"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
)]);
let new_font = g.apply_next_patches(&mut patch_data).unwrap();
let new_font = FontRef::new(&new_font).unwrap();
assert_eq!(
new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
TABLE_1_FINAL_STATE,
);
assert_eq!(
new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
TABLE_2_FINAL_STATE,
);
assert_eq!(
patch_data,
HashMap::from([(PatchUrl::new("foo/04"), UrlStatus::Applied,),])
);
}
#[test]
fn apply_patches_two_partial_invalidation() {
let mut ift_buffer = table_keyed_format2();
ift_buffer.write_at("encoding", 2u8);
let mut iftx_buffer = table_keyed_format2();
iftx_buffer.write_at("compat_id[0]", 2u32);
iftx_buffer.write_at("encoding", 2u8);
iftx_buffer.write_at("id_delta", Int24::new(2));
let font = base_font(Some(ift_buffer), Some(iftx_buffer));
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font.clone(), &Default::default(), &s).unwrap();
let mut patch_2 = table_keyed_patch();
patch_2.write_at("compat_id", 2u32);
patch_2.write_at("patch[0]", Tag::new(b"tab4"));
patch_2.write_at("patch[1]", Tag::new(b"tab5"));
let mut patch_data = HashMap::from([
(
PatchUrl::new("foo/04"),
UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
),
(
PatchUrl::new("foo/08"),
UrlStatus::Pending(patch_2.as_slice().to_vec()),
),
]);
let new_font = g.apply_next_patches(&mut patch_data).unwrap();
let new_font = FontRef::new(&new_font).unwrap();
assert_eq!(
new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
TABLE_1_FINAL_STATE,
);
assert_eq!(
new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
TABLE_2_FINAL_STATE,
);
assert_eq!(
new_font.table_data(Tag::new(b"tab4")).unwrap().as_bytes(),
font.table_data(Tag::new(b"tab4")).unwrap().as_bytes(),
);
assert_eq!(
new_font.table_data(Tag::new(b"tab5")).unwrap().as_bytes(),
font.table_data(Tag::new(b"tab5")).unwrap().as_bytes(),
);
}
#[test]
fn apply_patches_mixed() {
let mut ift_builder = table_keyed_format2();
ift_builder.write_at("encoding", 2u8);
let mut iftx_builder = table_keyed_format2();
iftx_builder.write_at("encoding", 3u8);
iftx_builder.write_at("compat_id[0]", 6u32);
iftx_builder.write_at("compat_id[1]", 7u32);
iftx_builder.write_at("compat_id[2]", 8u32);
iftx_builder.write_at("compat_id[3]", 9u32);
iftx_builder.write_at("id_delta", Int24::new(2));
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([
(IFT_TAG, ift_builder.as_slice()),
(IFTX_TAG, iftx_builder.as_slice()),
(Tag::new(b"tab1"), "abcdef\n".as_bytes()),
]),
);
let font = FontRef::new(font.as_slice()).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font.clone(), &Default::default(), &s).unwrap();
let patch_ift = table_keyed_patch();
let patch_iftx =
assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
let mut patch_data = HashMap::from([
(
PatchUrl::new("foo/04"),
UrlStatus::Pending(patch_ift.as_slice().to_vec()),
),
(
PatchUrl::new("foo/08"),
UrlStatus::Pending(patch_iftx.as_slice().to_vec()),
),
]);
let new_font = g.apply_next_patches(&mut patch_data).unwrap();
let new_font = FontRef::new(&new_font).unwrap();
assert_eq!(
new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
TABLE_1_FINAL_STATE,
);
assert_eq!(
new_font.table_data(Tag::new(b"glyf")).unwrap().as_bytes(),
font.table_data(Tag::new(b"glyf")).unwrap().as_bytes(),
);
}
#[test]
fn apply_patches_all_no_invalidation() {
let mut ift_builder = table_keyed_format2();
ift_builder.write_at("encoding", 3u8);
ift_builder.write_at("compat_id[0]", 6u32);
ift_builder.write_at("compat_id[1]", 7u32);
ift_builder.write_at("compat_id[2]", 8u32);
ift_builder.write_at("compat_id[3]", 9u32);
let mut iftx_builder = table_keyed_format2();
iftx_builder.write_at("encoding", 3u8);
iftx_builder.write_at("compat_id[0]", 7u32);
iftx_builder.write_at("compat_id[1]", 7u32);
iftx_builder.write_at("compat_id[2]", 8u32);
iftx_builder.write_at("compat_id[3]", 9u32);
iftx_builder.write_at("id_delta", Int24::new(2));
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([
(IFT_TAG, ift_builder.as_slice()),
(IFTX_TAG, iftx_builder.as_slice()),
]),
);
let font = FontRef::new(font.as_slice()).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
let patch1 =
assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
let mut patch2 = glyf_u16_glyph_patches();
patch2.write_at("gid_13", 14u16);
let mut header = glyph_keyed_patch_header();
header.write_at("compatibility_id", 7u32);
let patch2 = assemble_glyph_keyed_patch(header, patch2);
let mut patch_data = HashMap::from([
(
PatchUrl::new("foo/04"),
UrlStatus::Pending(patch1.as_slice().to_vec()),
),
(
PatchUrl::new("foo/08"),
UrlStatus::Pending(patch2.as_slice().to_vec()),
),
]);
let new_font = g.apply_next_patches(&mut patch_data).unwrap();
let new_font = FontRef::new(&new_font).unwrap();
let new_glyf: &[u8] = new_font.table_data(Tag::new(b"glyf")).unwrap().as_bytes();
assert_eq!(
&[
1, 2, 3, 4, 5, 0, 6, 7, 8, 0, b'a', b'b', b'c', 0, b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', 0, b'm', b'n', b'm', b'n', ],
new_glyf
);
assert_eq!(
patch_data,
HashMap::from([
(PatchUrl::new("foo/04"), UrlStatus::Applied,),
(PatchUrl::new("foo/08"), UrlStatus::Applied,),
])
);
let g = PatchGroup::select_next_patches(new_font, &Default::default(), &s).unwrap();
assert!(!g.has_urls());
}
#[test]
fn apply_patches_no_invalidation_duplicate_urls() {
let mut ift_builder = table_keyed_format2();
ift_builder.write_at("encoding", 3u8);
ift_builder.write_at("compat_id[0]", 6u32);
ift_builder.write_at("compat_id[1]", 7u32);
ift_builder.write_at("compat_id[2]", 8u32);
ift_builder.write_at("compat_id[3]", 9u32);
ift_builder.write_at("entry_count", Uint24::new(2));
let ift_builder = ift_builder
.push(0b00100100u8) .push(Int24::new(-2)) .push(100u16) .extend([0b00001101, 0b00000011, 0b00110001u8]);
let mut iftx_builder = table_keyed_format2();
iftx_builder.write_at("encoding", 3u8);
iftx_builder.write_at("compat_id[0]", 0u32);
iftx_builder.write_at("compat_id[1]", 0u32);
iftx_builder.write_at("compat_id[2]", 0u32);
iftx_builder.write_at("compat_id[3]", 2u32);
iftx_builder.write_at("bias", 100u16);
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([
(IFT_TAG, ift_builder.as_slice()),
(IFTX_TAG, iftx_builder.as_slice()),
]),
);
let font = FontRef::new(font.as_slice()).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
let patch =
assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
let mut patch_data = HashMap::from([(
PatchUrl::new("foo/04"),
UrlStatus::Pending(patch.as_slice().to_vec()),
)]);
let new_font = g.apply_next_patches(&mut patch_data).unwrap();
let new_font = FontRef::new(&new_font).unwrap();
let new_glyf: &[u8] = new_font.table_data(Tag::new(b"glyf")).unwrap().as_bytes();
assert_eq!(
&[
1, 2, 3, 4, 5, 0, 6, 7, 8, 0, b'a', b'b', b'c', 0, b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', 0, b'm', b'n', ],
new_glyf
);
assert_eq!(
patch_data,
HashMap::from([(PatchUrl::new("foo/04"), UrlStatus::Applied,),])
);
let all = SubsetDefinition::all();
let group = PatchGroup::select_next_patches(new_font, &Default::default(), &all).unwrap();
let mut info = patch_info_iftx("foo/04");
info.application_flag_bit_indices.clear();
info.application_flag_bit_indices.insert(334);
assert_eq!(
group.patches.unwrap(),
CompatibleGroup::Mixed {
ift: ScopedGroup::NoInvalidation(Default::default()),
iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
OrderedPatchUrl::url(0, "foo/04"),
NoInvalidationPatch(info)
)])),
}
);
}
#[test]
fn tables_have_same_compat_id() {
let ift_buffer = table_keyed_format2();
let iftx_buffer = table_keyed_format2();
let font = base_font(Some(ift_buffer), Some(iftx_buffer));
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let g = PatchGroup::select_next_patches(font.clone(), &Default::default(), &s);
assert!(g.is_err(), "did not fail as expected.");
if let Err(err) = g {
assert_eq!(ReadError::ValidationError, err);
}
}
#[test]
fn invalid_url_templates() {
let mut buffer = table_keyed_format2();
buffer.write_at("url_template_var_end", b'~');
let font = base_font(Some(buffer), None);
let font = FontRef::new(&font).unwrap();
let s = SubsetDefinition::codepoints([5].into_iter().collect());
let Err(err) = PatchGroup::select_next_patches(font, &Default::default(), &s) else {
panic!("Should have failed")
};
assert_eq!(
err,
ReadError::MalformedData("Failed to expand url template in format 2 table.")
);
}
#[test]
fn select_next_patches_ordering_for_non_invalidating() {
let mut ift_builder = custom_ids_format2();
ift_builder.write_at("entries[0].id_delta", Int24::new(30)); ift_builder.write_at("entries[1].id_delta", Int24::new(-10));
let mut iftx_builder = custom_ids_format2();
iftx_builder.write_at("compat_id[0]", 5u32);
iftx_builder.write_at("compat_id[1]", 6u32);
iftx_builder.write_at("compat_id[2]", 7u32);
iftx_builder.write_at("compat_id[3]", 8u32);
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([
(IFT_TAG, ift_builder.as_slice()),
(IFTX_TAG, iftx_builder.as_slice()),
]),
);
let font = FontRef::new(font.as_slice()).unwrap();
let s = SubsetDefinition::codepoints([10].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
let urls: Vec<String> = g.urls().map(|p| p.as_ref().to_string()).collect();
let expected_urls: Vec<String> = [16, 12, 21, 0, 6, 15]
.into_iter()
.map(|index| PatchUrl::expand_template(RELATIVE_URL_TEMPLATE, &PatchId::Numeric(index)))
.map(|url| url.unwrap())
.map(|url| url.as_ref().to_string())
.collect();
assert_eq!(urls, expected_urls);
}
#[test]
fn select_next_patches_ordering_for_non_invalidating_with_duplicates() {
let mut ift_builder = custom_ids_format2();
ift_builder.write_at("entries[0].id_delta", Int24::new(30)); ift_builder.write_at("entries[1].id_delta", Int24::new(-20));
let mut iftx_builder = custom_ids_format2();
iftx_builder.write_at("compat_id[0]", 5u32);
iftx_builder.write_at("compat_id[1]", 6u32);
iftx_builder.write_at("compat_id[2]", 7u32);
iftx_builder.write_at("compat_id[3]", 8u32);
let font = test_font_for_patching_with_loca_mod(
true,
|_| {},
HashMap::from([
(IFT_TAG, ift_builder.as_slice()),
(IFTX_TAG, iftx_builder.as_slice()),
]),
);
let font = FontRef::new(font.as_slice()).unwrap();
let s = SubsetDefinition::codepoints([10].into_iter().collect());
let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
let urls: Vec<String> = g.urls().map(|p| p.as_ref().to_string()).collect();
let expected_urls: Vec<String> = [16, 7, 0, 6, 15]
.into_iter()
.map(|index| PatchUrl::expand_template(RELATIVE_URL_TEMPLATE, &PatchId::Numeric(index)))
.map(|url| url.unwrap())
.map(|url| url.as_ref().to_string())
.collect();
assert_eq!(urls, expected_urls);
}
}