1use read_fonts::{
7 collections::IntSet, tables::ift::CompatibilityId, FontRef, ReadError, TableProvider,
8};
9use shared_brotli_patch_decoder::{BuiltInBrotliDecoder, SharedBrotliDecoder};
10use std::{
11 cmp::Ordering,
12 collections::{BTreeMap, BTreeSet, HashMap},
13};
14
15use crate::{
16 font_patch::{IncrementalFontPatchBase, PatchingError},
17 patchmap::{
18 intersecting_patches, IftTableTag, IntersectionInfo, PatchFormat, PatchMapEntry, PatchUrl,
19 SubsetDefinition,
20 },
21};
22
23#[derive(Clone)]
32pub struct PatchGroup<'a> {
33 font: FontRef<'a>,
34 patches: Option<CompatibleGroup>,
35
36 preload_urls: BTreeSet<PatchUrl>,
39}
40
41impl std::fmt::Debug for PatchGroup<'_> {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("PatchGroup")
46 .field("patches", &self.patches)
47 .field("preload_urls", &self.preload_urls)
48 .finish_non_exhaustive()
49 }
50}
51
52enum Selection {
53 IftPartial,
54 IftxPartial,
55 IftNo,
56 IftxNo,
57 Done,
58}
59
60impl Selection {
61 fn tag_index(&self) -> usize {
62 match self {
63 Self::IftPartial | Self::IftNo => 0,
64 _ => 1,
65 }
66 }
67
68 fn next(self) -> Selection {
69 match self {
70 Self::IftPartial => Self::IftxPartial,
71 Self::IftxPartial => Self::IftNo,
72 Self::IftNo => Self::IftxNo,
73 Self::IftxNo => Self::Done,
74 Self::Done => Self::Done,
75 }
76 }
77}
78
79impl PatchGroup<'_> {
80 pub fn select_next_patches<'b>(
87 ift_font: FontRef<'b>,
88 patch_data: &HashMap<PatchUrl, UrlStatus>,
89 subset_definition: &SubsetDefinition,
90 ) -> Result<PatchGroup<'b>, ReadError> {
91 let candidates = intersecting_patches(&ift_font, subset_definition)?;
92 if candidates.is_empty() {
93 return Ok(PatchGroup {
94 font: ift_font,
95 patches: None,
96 preload_urls: Default::default(),
97 });
98 }
99
100 let ift_compat_id = ift_font.ift().ok().map(|t| t.compatibility_id());
101 let iftx_compat_id = ift_font.iftx().ok().map(|t| t.compatibility_id());
102 if ift_compat_id == iftx_compat_id {
103 return Err(ReadError::ValidationError);
106 }
107
108 let (compat_group, preload_urls) = Self::select_next_patches_from_candidates(
109 candidates,
110 patch_data,
111 ift_compat_id,
112 iftx_compat_id,
113 )?;
114
115 Ok(PatchGroup {
116 font: ift_font,
117 patches: Some(compat_group),
118 preload_urls,
119 })
120 }
121
122 pub fn urls(&self) -> impl Iterator<Item = &PatchUrl> {
124 self.invalidating_patch_iter()
125 .chain(self.non_invalidating_patch_iter())
126 .map(|info| &info.url)
127 .chain(self.preload_urls.iter())
128 }
129
130 pub fn has_urls(&self) -> bool {
132 let Some(patches) = &self.patches else {
133 return !self.preload_urls.is_empty();
134 };
135 match patches {
136 CompatibleGroup::Full(FullInvalidationPatch(_)) => true,
137 CompatibleGroup::Mixed { ift, iftx } => {
138 ift.has_urls() || iftx.has_urls() || !self.preload_urls.is_empty()
139 }
140 }
141 }
142
143 pub fn next_invalidating_patch(&self) -> Option<&PatchInfo> {
144 self.invalidating_patch_iter().next()
145 }
146
147 fn invalidating_patch_iter(&self) -> impl Iterator<Item = &PatchInfo> {
148 let full = match &self.patches {
149 Some(CompatibleGroup::Full(info)) => Some(&info.0),
150 _ => None,
151 };
152
153 let partial_1 = match &self.patches {
154 Some(CompatibleGroup::Mixed {
155 ift: ScopedGroup::PartialInvalidation(v),
156 iftx: _,
157 }) => Some(&v.0),
158 _ => None,
159 };
160
161 let partial_2 = match &self.patches {
162 Some(CompatibleGroup::Mixed {
163 ift: _,
164 iftx: ScopedGroup::PartialInvalidation(v),
165 }) => Some(&v.0),
166 _ => None,
167 };
168
169 full.into_iter().chain(partial_1).chain(partial_2)
170 }
171
172 fn non_invalidating_patch_iter(&self) -> impl Iterator<Item = &PatchInfo> {
173 let ift = match &self.patches {
174 Some(CompatibleGroup::Mixed { ift, iftx: _ }) => Some(ift),
175 _ => None,
176 };
177 let iftx = match &self.patches {
178 Some(CompatibleGroup::Mixed { ift: _, iftx }) => Some(iftx),
179 _ => None,
180 };
181
182 let it1 = ift
183 .into_iter()
184 .flat_map(|scope| scope.no_invalidation_iter());
185 let it2 = iftx
186 .into_iter()
187 .flat_map(|scope| scope.no_invalidation_iter());
188
189 it1.chain(it2)
190 }
191
192 fn select_next_patches_from_candidates(
193 candidates: Vec<PatchMapEntry>,
194 patch_data: &HashMap<PatchUrl, UrlStatus>,
195 ift_compat_id: Option<CompatibilityId>,
196 iftx_compat_id: Option<CompatibilityId>,
197 ) -> Result<(CompatibleGroup, BTreeSet<PatchUrl>), ReadError> {
198 let GroupingByInvalidation {
217 full_invalidation,
218 partial_invalidation_ift,
219 partial_invalidation_iftx,
220 no_invalidation_ift,
221 no_invalidation_iftx,
222 } = GroupingByInvalidation::group_patches(
223 candidates,
224 patch_data,
225 ift_compat_id,
226 iftx_compat_id,
227 );
228
229 let mut combined_preload_urls: BTreeSet<PatchUrl> = Default::default();
230
231 if let Some(patch) = Self::select_invalidating_candidate(full_invalidation) {
233 combined_preload_urls = patch.preload_urls.iter().cloned().collect();
235 combined_preload_urls.remove(&patch.patch_info.url);
236 return Ok((CompatibleGroup::Full(patch.into()), combined_preload_urls));
237 }
238
239 let mut selection_mode = Selection::IftPartial;
246 let mut scoped_groups: [Option<ScopedGroup>; 2] = [None, None];
247
248 let mut partial_invalidation_candidates =
249 [partial_invalidation_ift, partial_invalidation_iftx];
250 let mut no_invalidation_candidates = [no_invalidation_ift, no_invalidation_iftx];
251 let mut selected_urls: BTreeSet<PatchUrl> = Default::default();
252
253 loop {
254 let tag_index = selection_mode.tag_index();
255 match selection_mode {
256 Selection::IftPartial | Selection::IftxPartial => {
257 let mut candidates: Vec<CandidatePatch> = vec![];
259 std::mem::swap(
260 &mut candidates,
261 &mut partial_invalidation_candidates[tag_index],
262 );
263
264 scoped_groups[tag_index] = Self::select_invalidating_candidate(
265 candidates
266 .into_iter()
267 .filter(|c| !selected_urls.contains(&c.patch_info.url)),
268 )
269 .map(|patch| {
270 combined_preload_urls.extend(patch.preload_urls.iter().cloned());
271 selected_urls.insert(patch.patch_info.url.clone());
272 ScopedGroup::PartialInvalidation(patch.into())
273 })
274 }
275 Selection::IftNo | Selection::IftxNo => {
276 if scoped_groups[tag_index].is_none() {
277 let mut candidates: BTreeMap<PatchUrl, CandidateNoInvalidationPatch> =
278 Default::default();
279 std::mem::swap(&mut candidates, &mut no_invalidation_candidates[tag_index]);
280
281 scoped_groups[tag_index] = Some(ScopedGroup::NoInvalidation(
282 Self::filter_and_extract_preloads(
283 candidates,
284 &mut selected_urls,
285 &mut combined_preload_urls,
286 ),
287 ))
288 }
289 }
290 Selection::Done => break,
291 };
292
293 selection_mode = selection_mode.next();
294 }
295
296 combined_preload_urls.retain(|url| !selected_urls.contains(url));
298
299 let [Some(ift), Some(iftx)] = scoped_groups else {
300 return Err(ReadError::MalformedData(
301 "Failed invariant. Both arms of the mixed compat group should always be filled.",
302 ));
303 };
304
305 Ok((CompatibleGroup::Mixed { ift, iftx }, combined_preload_urls))
306 }
307
308 fn filter_and_extract_preloads(
309 candidates: BTreeMap<PatchUrl, CandidateNoInvalidationPatch>,
310 previously_selected_urls: &mut BTreeSet<PatchUrl>,
311 preloads: &mut BTreeSet<PatchUrl>,
312 ) -> BTreeMap<OrderedPatchUrl, NoInvalidationPatch> {
313 preloads.extend(
314 candidates
315 .values()
316 .flat_map(|candidate| candidate.preload_urls.iter().cloned()),
317 );
318 let filtered: BTreeMap<OrderedPatchUrl, NoInvalidationPatch> = candidates
319 .into_iter()
320 .filter(|(k, _)| !previously_selected_urls.contains(k))
321 .map(|(k, v)| {
322 (
323 OrderedPatchUrl(v.entry_order, k),
324 NoInvalidationPatch(v.patch_info),
325 )
326 })
327 .collect();
328
329 for (url, _) in filtered.iter() {
330 previously_selected_urls.insert(url.1.clone());
331 }
332
333 filtered
334 }
335
336 fn select_invalidating_candidate<T>(candidates: T) -> Option<CandidatePatch>
340 where
341 T: IntoIterator<Item = CandidatePatch>,
342 {
343 candidates.into_iter().max()
351 }
352
353 pub fn apply_next_patches(
357 self,
358 patch_data: &mut HashMap<PatchUrl, UrlStatus>,
359 ) -> Result<Vec<u8>, PatchingError> {
360 self.apply_next_patches_with_decoder(patch_data, &BuiltInBrotliDecoder)
361 }
362
363 pub fn apply_next_patches_with_decoder<D: SharedBrotliDecoder>(
367 self,
368 patch_data: &mut HashMap<PatchUrl, UrlStatus>,
369 brotli_decoder: &D,
370 ) -> Result<Vec<u8>, PatchingError> {
371 if let Some(patch) = self.next_invalidating_patch() {
372 let entry = patch_data
373 .get_mut(&patch.url)
374 .ok_or(PatchingError::MissingPatches)?;
375
376 match entry {
377 UrlStatus::Pending(patch_data) => {
378 let r = self
379 .font
380 .apply_table_keyed_patch(patch, patch_data, brotli_decoder)?;
381 *entry = UrlStatus::Applied;
382 return Ok(r);
383 }
384 UrlStatus::Applied => {} }
386 }
387
388 let new_font = {
391 let mut accumulated_info: Vec<(&PatchInfo, &[u8])> = vec![];
392 for info in self.non_invalidating_patch_iter() {
393 let data = patch_data
394 .get(&info.url)
395 .ok_or(PatchingError::MissingPatches)?;
396
397 match data {
398 UrlStatus::Pending(data) => accumulated_info.push((info, data)),
399 UrlStatus::Applied => {} }
401 }
402
403 if accumulated_info.is_empty() {
404 return Err(PatchingError::EmptyPatchList);
405 }
406
407 self.font
408 .apply_glyph_keyed_patches(accumulated_info.into_iter(), brotli_decoder)?
409 };
410
411 for info in self.non_invalidating_patch_iter() {
412 if let Some(status) = patch_data.get_mut(&info.url) {
413 *status = UrlStatus::Applied;
414 };
415 }
416
417 Ok(new_font)
418 }
419}
420
421#[derive(Default)]
422struct GroupingByInvalidation {
423 full_invalidation: Vec<CandidatePatch>,
424 partial_invalidation_ift: Vec<CandidatePatch>,
425 partial_invalidation_iftx: Vec<CandidatePatch>,
426 no_invalidation_ift: BTreeMap<PatchUrl, CandidateNoInvalidationPatch>,
428 no_invalidation_iftx: BTreeMap<PatchUrl, CandidateNoInvalidationPatch>,
429}
430
431impl GroupingByInvalidation {
432 fn group_patches(
433 candidates: Vec<PatchMapEntry>,
434 patch_data: &HashMap<PatchUrl, UrlStatus>,
435 ift_compat_id: Option<CompatibilityId>,
436 iftx_compat_id: Option<CompatibilityId>,
437 ) -> GroupingByInvalidation {
438 let mut result: GroupingByInvalidation = Default::default();
439
440 for entry in candidates.into_iter() {
441 match entry.format {
444 PatchFormat::TableKeyed {
445 fully_invalidating: true,
446 } => result
447 .full_invalidation
448 .push(CandidatePatch::from_entry(entry, patch_data)),
449 PatchFormat::TableKeyed {
450 fully_invalidating: false,
451 } => {
452 if Some(entry.expected_compat_id()) == ift_compat_id.as_ref() {
453 result
454 .partial_invalidation_ift
455 .push(CandidatePatch::from_entry(entry, patch_data))
456 } else if Some(entry.expected_compat_id()) == iftx_compat_id.as_ref() {
457 result
458 .partial_invalidation_iftx
459 .push(CandidatePatch::from_entry(entry, patch_data))
460 }
461 }
462 PatchFormat::GlyphKeyed => {
463 let mapping = if Some(entry.expected_compat_id()) == ift_compat_id.as_ref() {
464 Some(&mut result.no_invalidation_ift)
465 } else if Some(entry.expected_compat_id()) == iftx_compat_id.as_ref() {
466 Some(&mut result.no_invalidation_iftx)
467 } else {
468 None
469 };
470 if let Some(mapping) = mapping {
471 mapping
472 .entry(entry.url().clone())
473 .and_modify(|existing| {
474 if entry.intersection_info.entry_order() < existing.entry_order {
477 *existing = entry.clone().into()
478 }
479 })
480 .or_insert_with(|| entry.into());
481 }
482 }
483 }
484 }
485
486 result
487 }
488}
489
490#[derive(PartialEq, Eq, Debug, Clone)]
492pub enum UrlStatus {
493 Applied,
494 Pending(Vec<u8>),
495}
496
497#[derive(PartialEq, Eq, Debug, Clone)]
499pub struct PatchInfo {
500 pub(crate) url: PatchUrl,
501 pub(crate) source_table: IftTableTag,
502 pub(crate) application_flag_bit_indices: IntSet<u32>,
503}
504
505impl From<PatchMapEntry> for PatchInfo {
506 fn from(value: PatchMapEntry) -> Self {
507 PatchInfo {
508 url: value.url,
509 source_table: value.source_table,
510 application_flag_bit_indices: value.application_bit_indices,
511 }
512 }
513}
514
515impl PatchInfo {
516 pub(crate) fn tag(&self) -> &IftTableTag {
517 &self.source_table
518 }
519
520 pub(crate) fn application_flag_bit_indices(&self) -> impl Iterator<Item = u32> + '_ {
521 self.application_flag_bit_indices.iter()
522 }
523
524 pub fn url(&self) -> &str {
525 self.url.as_ref()
526 }
527}
528
529#[derive(PartialEq, Eq)]
531struct CandidatePatch {
532 intersection_info: IntersectionInfo,
533 patch_info: PatchInfo,
534 preload_urls: Vec<PatchUrl>,
535 already_loaded: bool,
536}
537
538struct CandidateNoInvalidationPatch {
539 patch_info: PatchInfo,
540 entry_order: usize,
541 preload_urls: Vec<PatchUrl>,
542}
543
544impl CandidatePatch {
545 fn from_entry(
546 value: PatchMapEntry,
547 patch_data: &HashMap<PatchUrl, UrlStatus>,
548 ) -> CandidatePatch {
549 let patch_info = PatchInfo {
550 url: value.url,
551 source_table: value.source_table,
552 application_flag_bit_indices: value.application_bit_indices,
553 };
554 let already_loaded = patch_data.contains_key(&patch_info.url);
555 Self {
556 intersection_info: value.intersection_info,
557 patch_info,
558 preload_urls: value.preload_urls,
559 already_loaded,
560 }
561 }
562}
563
564impl PartialOrd for CandidatePatch {
565 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
566 Some(self.cmp(other))
567 }
568}
569
570impl Ord for CandidatePatch {
571 fn cmp(&self, other: &Self) -> Ordering {
572 match (self.already_loaded, other.already_loaded) {
578 (true, false) => Ordering::Greater,
579 (false, true) => Ordering::Less,
580 _ => self.intersection_info.cmp(&other.intersection_info),
581 }
582 }
583}
584
585impl From<PatchMapEntry> for CandidateNoInvalidationPatch {
586 fn from(mut value: PatchMapEntry) -> Self {
587 let mut preload_urls: Vec<PatchUrl> = vec![];
588 std::mem::swap(&mut preload_urls, &mut value.preload_urls);
589 let entry_order = value.intersection_info.entry_order();
590 Self {
591 patch_info: value.into(),
592 entry_order,
593 preload_urls,
594 }
595 }
596}
597
598#[derive(PartialEq, Eq, Debug, Clone)]
600struct NoInvalidationPatch(PatchInfo);
601
602#[derive(PartialEq, Eq, Debug, Clone)]
604struct PartialInvalidationPatch(PatchInfo);
605
606impl From<CandidatePatch> for PartialInvalidationPatch {
607 fn from(value: CandidatePatch) -> Self {
608 Self(value.patch_info)
609 }
610}
611
612#[derive(PartialEq, Eq, Debug, Clone)]
614struct FullInvalidationPatch(PatchInfo);
615
616impl From<CandidatePatch> for FullInvalidationPatch {
617 fn from(value: CandidatePatch) -> Self {
618 Self(value.patch_info)
619 }
620}
621
622#[derive(PartialEq, Eq, Debug, Clone)]
625enum CompatibleGroup {
626 Full(FullInvalidationPatch),
627 Mixed { ift: ScopedGroup, iftx: ScopedGroup },
628}
629
630#[derive(PartialEq, Eq, Debug, Clone)]
633enum ScopedGroup {
634 PartialInvalidation(PartialInvalidationPatch),
635 NoInvalidation(BTreeMap<OrderedPatchUrl, NoInvalidationPatch>),
636}
637
638impl ScopedGroup {
639 fn has_urls(&self) -> bool {
640 match self {
641 ScopedGroup::PartialInvalidation(PartialInvalidationPatch(_)) => true,
642 ScopedGroup::NoInvalidation(url_map) => !url_map.is_empty(),
643 }
644 }
645
646 fn no_invalidation_iter(&self) -> impl Iterator<Item = &PatchInfo> {
647 match self {
648 ScopedGroup::PartialInvalidation(_) => NoInvalidationPatchesIter { it: None },
649 ScopedGroup::NoInvalidation(map) => NoInvalidationPatchesIter {
650 it: Some(map.values()),
651 },
652 }
653 }
654}
655
656#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
662pub struct OrderedPatchUrl(usize, PatchUrl);
663
664struct NoInvalidationPatchesIter<'a, T>
665where
666 T: Iterator<Item = &'a NoInvalidationPatch>,
667{
668 it: Option<T>,
669}
670
671impl<'a, T> Iterator for NoInvalidationPatchesIter<'a, T>
672where
673 T: Iterator<Item = &'a NoInvalidationPatch>,
674{
675 type Item = &'a PatchInfo;
676
677 fn next(&mut self) -> Option<Self::Item> {
678 let it = self.it.as_mut()?;
679 Some(&it.next()?.0)
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use std::collections::HashMap;
686
687 use super::*;
688 use crate::{
689 glyph_keyed::tests::assemble_glyph_keyed_patch, patchmap::PatchId,
690 testdata::test_font_for_patching_with_loca_mod,
691 };
692 use font_test_data::{
693 bebuffer::BeBuffer,
694 ift::{
695 custom_ids_format2, glyf_u16_glyph_patches, glyph_keyed_patch_header,
696 table_keyed_format2, table_keyed_patch, ABSOLUTE_URL_TEMPLATE, RELATIVE_URL_TEMPLATE,
697 },
698 };
699
700 use font_types::{Int24, Tag, Uint24};
701
702 use read_fonts::{
703 tables::ift::{IFTX_TAG, IFT_TAG},
704 FontRef,
705 };
706
707 use write_fonts::FontBuilder;
708
709 const TABLE_1_FINAL_STATE: &[u8] = "hijkabcdeflmnohijkabcdeflmno\n".as_bytes();
710 const TABLE_2_FINAL_STATE: &[u8] = "foobarbaz foobarbaz foobarbaz\n".as_bytes();
711
712 impl PatchUrl {
713 fn new(url: &str) -> Self {
714 Self(url.to_string())
715 }
716 }
717
718 impl OrderedPatchUrl {
719 fn url(order: usize, url: &str) -> Self {
720 OrderedPatchUrl(order, PatchUrl(url.to_string()))
721 }
722 }
723
724 fn base_font(ift: Option<BeBuffer>, iftx: Option<BeBuffer>) -> Vec<u8> {
725 let mut font_builder = FontBuilder::new();
726
727 if let Some(buffer) = &ift {
728 font_builder.add_raw(IFT_TAG, buffer.as_slice());
729 }
730 if let Some(buffer) = &iftx {
731 font_builder.add_raw(IFTX_TAG, buffer.as_slice());
732 }
733
734 font_builder.add_raw(Tag::new(b"tab1"), "abcdef\n".as_bytes());
735 font_builder.add_raw(Tag::new(b"tab2"), "foobar\n".as_bytes());
736 font_builder.add_raw(Tag::new(b"tab4"), "abcdef\n".as_bytes());
737 font_builder.add_raw(Tag::new(b"tab5"), "foobar\n".as_bytes());
738 font_builder.build()
739 }
740
741 fn cid_1() -> CompatibilityId {
742 CompatibilityId::from_u32s([0, 0, 0, 1])
743 }
744
745 fn cid_2() -> CompatibilityId {
746 CompatibilityId::from_u32s([0, 0, 0, 2])
747 }
748
749 fn p(index: u32, table: IftTableTag, format: PatchFormat) -> PatchMapEntry {
750 let url =
751 PatchUrl::expand_template(ABSOLUTE_URL_TEMPLATE, &PatchId::Numeric(index)).unwrap();
752 let mut e = url.into_format_1_entry(table, format, Default::default());
753 e.application_bit_indices.insert(42);
754 e
755 }
756
757 fn p1_full() -> PatchMapEntry {
758 p(
759 1,
760 IftTableTag::Ift(cid_1()),
761 PatchFormat::TableKeyed {
762 fully_invalidating: true,
763 },
764 )
765 }
766
767 fn p2_partial_c1() -> PatchMapEntry {
768 p(
769 2,
770 IftTableTag::Ift(cid_1()),
771 PatchFormat::TableKeyed {
772 fully_invalidating: false,
773 },
774 )
775 }
776
777 fn p2_partial_c2() -> PatchMapEntry {
778 p(
779 2,
780 IftTableTag::Iftx(cid_2()),
781 PatchFormat::TableKeyed {
782 fully_invalidating: false,
783 },
784 )
785 }
786
787 fn p2_no_c2() -> PatchMapEntry {
788 p(2, IftTableTag::Iftx(cid_2()), PatchFormat::GlyphKeyed)
789 }
790
791 fn p3_partial_c2() -> PatchMapEntry {
792 p(
793 3,
794 IftTableTag::Iftx(cid_2()),
795 PatchFormat::TableKeyed {
796 fully_invalidating: false,
797 },
798 )
799 }
800
801 fn p3_no_c1() -> PatchMapEntry {
802 p(3, IftTableTag::Ift(cid_1()), PatchFormat::GlyphKeyed)
803 }
804
805 fn p4_no_c1() -> PatchMapEntry {
806 p(4, IftTableTag::Ift(cid_1()), PatchFormat::GlyphKeyed)
807 }
808
809 fn p4_no_c2() -> PatchMapEntry {
810 p(4, IftTableTag::Iftx(cid_2()), PatchFormat::GlyphKeyed)
811 }
812
813 fn p5_no_c2() -> PatchMapEntry {
814 p(5, IftTableTag::Iftx(cid_2()), PatchFormat::GlyphKeyed)
815 }
816
817 fn full(index: u32, codepoints: u64) -> PatchMapEntry {
818 let url =
819 PatchUrl::expand_template(ABSOLUTE_URL_TEMPLATE, &PatchId::Numeric(index)).unwrap();
820 let mut e = url.into_format_1_entry(
821 IftTableTag::Ift(cid_1()),
822 PatchFormat::TableKeyed {
823 fully_invalidating: true,
824 },
825 IntersectionInfo::new(codepoints, 0, 0),
826 );
827 e.application_bit_indices.insert(42);
828 e
829 }
830
831 fn partial(index: u32, compat_id: CompatibilityId, codepoints: u64) -> PatchMapEntry {
832 let tag = if compat_id == cid_1() {
833 IftTableTag::Ift(compat_id)
834 } else {
835 IftTableTag::Iftx(compat_id)
836 };
837 let url =
838 PatchUrl::expand_template(ABSOLUTE_URL_TEMPLATE, &PatchId::Numeric(index)).unwrap();
839 let mut e = url.into_format_1_entry(
840 tag,
841 PatchFormat::TableKeyed {
842 fully_invalidating: false,
843 },
844 IntersectionInfo::new(codepoints, 0, 0),
845 );
846 e.application_bit_indices.insert(42);
847 e
848 }
849
850 fn patch_info_ift(url: &str) -> PatchInfo {
851 let mut application_flag_bit_indices = IntSet::<u32>::empty();
852 application_flag_bit_indices.insert(42);
853 PatchInfo {
854 url: PatchUrl::new(url),
855 application_flag_bit_indices,
856 source_table: IftTableTag::Ift(cid_1()),
857 }
858 }
859
860 fn patch_info_iftx(url: &str) -> PatchInfo {
861 let mut application_flag_bit_indices = IntSet::<u32>::empty();
862 application_flag_bit_indices.insert(42);
863 PatchInfo {
864 url: PatchUrl::new(url),
865 application_flag_bit_indices,
866 source_table: IftTableTag::Iftx(cid_2()),
867 }
868 }
869
870 #[test]
871 fn full_invalidation() {
872 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
873 vec![p1_full()],
874 &Default::default(),
875 Some(cid_1()),
876 Some(cid_2()),
877 )
878 .unwrap();
879
880 assert_eq!(
881 group,
882 CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04")))
883 );
884 assert!(preloads.is_empty());
885
886 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
887 vec![
888 p1_full(),
889 p2_partial_c1(),
890 p3_partial_c2(),
891 p4_no_c1(),
892 p5_no_c2(),
893 ],
894 &Default::default(),
895 Some(cid_1()),
896 Some(cid_2()),
897 )
898 .unwrap();
899
900 assert_eq!(
901 group,
902 CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04"),))
903 );
904 assert!(preloads.is_empty());
905 }
906
907 fn preload_list(urls: &[String]) -> Vec<PatchUrl> {
908 urls.iter().map(|url| PatchUrl::new(url)).collect()
909 }
910
911 #[test]
912 fn full_invalidation_with_preloads() {
913 let expected_preloads = [PatchUrl::new("abc"), PatchUrl::new("def")];
914 let mut p1_full = p1_full();
915 p1_full.preload_urls.extend(expected_preloads.clone());
916
917 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
918 vec![p1_full.clone()],
919 &Default::default(),
920 Some(cid_1()),
921 Some(cid_2()),
922 )
923 .unwrap();
924
925 assert_eq!(
926 group,
927 CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04")))
928 );
929 assert_eq!(preloads, BTreeSet::from(expected_preloads.clone()));
930
931 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
932 vec![
933 p1_full,
934 p2_partial_c1(),
935 p3_partial_c2(),
936 p4_no_c1(),
937 p5_no_c2(),
938 ],
939 &Default::default(),
940 Some(cid_1()),
941 Some(cid_2()),
942 )
943 .unwrap();
944
945 assert_eq!(
946 group,
947 CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04"),))
948 );
949 assert_eq!(preloads, BTreeSet::from(expected_preloads));
950 }
951
952 #[test]
953 fn full_invalidation_with_preloads_removes_duplicate_urls() {
954 let expected_preloads = [PatchUrl::new("abc"), PatchUrl::new("def")];
955 let mut p1_full = p1_full();
956 p1_full.preload_urls.extend(expected_preloads.clone());
957 p1_full
958 .preload_urls
959 .extend(preload_list(&["//foo.bar/04".to_string()]));
960
961 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
962 vec![p1_full.clone()],
963 &Default::default(),
964 Some(cid_1()),
965 Some(cid_2()),
966 )
967 .unwrap();
968
969 assert_eq!(
970 group,
971 CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/04")))
972 );
973 assert_eq!(preloads, BTreeSet::from(expected_preloads.clone()));
974 }
975
976 #[test]
977 fn full_invalidation_selection_order() {
978 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
979 vec![full(3, 9), full(1, 7), full(2, 24)],
980 &Default::default(),
981 Some(cid_1()),
982 Some(cid_2()),
983 )
984 .unwrap();
985
986 assert_eq!(
987 group,
988 CompatibleGroup::Full(FullInvalidationPatch(patch_info_ift("//foo.bar/08")))
989 );
990 assert!(preloads.is_empty());
991 }
992
993 #[test]
994 fn partial_invalidation_selection_order() {
995 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
997 vec![
998 partial(3, cid_1(), 9),
999 partial(1, cid_1(), 23),
1000 partial(2, cid_1(), 24),
1001 ],
1002 &Default::default(),
1003 Some(cid_1()),
1004 Some(cid_2()),
1005 )
1006 .unwrap();
1007
1008 assert_eq!(
1009 group,
1010 CompatibleGroup::Mixed {
1011 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1012 "//foo.bar/08"
1013 ),)),
1014 iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
1015 }
1016 );
1017 assert!(preloads.is_empty());
1018
1019 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1021 vec![
1022 partial(4, cid_2(), 1),
1023 partial(5, cid_2(), 22),
1024 partial(6, cid_2(), 2),
1025 ],
1026 &Default::default(),
1027 Some(cid_1()),
1028 Some(cid_2()),
1029 )
1030 .unwrap();
1031
1032 assert_eq!(
1033 group,
1034 CompatibleGroup::Mixed {
1035 ift: ScopedGroup::NoInvalidation(BTreeMap::default()),
1036
1037 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1038 "//foo.bar/0K"
1039 ),)),
1040 }
1041 );
1042 assert!(preloads.is_empty());
1043
1044 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1046 vec![
1047 partial(3, cid_1(), 9),
1048 partial(1, cid_1(), 23),
1049 partial(2, cid_1(), 24),
1050 partial(4, cid_2(), 1),
1051 partial(5, cid_2(), 22),
1052 partial(6, cid_2(), 2),
1053 ],
1054 &Default::default(),
1055 Some(cid_1()),
1056 Some(cid_2()),
1057 )
1058 .unwrap();
1059
1060 assert_eq!(
1061 group,
1062 CompatibleGroup::Mixed {
1063 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1064 "//foo.bar/08"
1065 ),)),
1066 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1067 "//foo.bar/0K"
1068 ),)),
1069 }
1070 );
1071 assert!(preloads.is_empty());
1072 }
1073
1074 #[test]
1075 fn partial_invalidation_with_preloaded() {
1076 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1082 vec![
1083 partial(3, cid_1(), 9),
1084 partial(1, cid_1(), 23),
1085 partial(2, cid_1(), 24),
1086 ],
1087 &HashMap::from([(PatchUrl::new("//foo.bar/0C"), UrlStatus::Pending(vec![]))]),
1089 Some(cid_1()),
1090 Some(cid_2()),
1091 )
1092 .unwrap();
1093
1094 assert_eq!(
1095 group,
1096 CompatibleGroup::Mixed {
1097 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1098 "//foo.bar/0C"
1099 ),)),
1100 iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
1101 }
1102 );
1103 assert!(preloads.is_empty());
1104
1105 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1107 vec![
1108 partial(3, cid_1(), 9),
1109 partial(1, cid_1(), 23),
1110 partial(2, cid_1(), 24),
1111 ],
1112 &HashMap::from([
1114 (PatchUrl::new("//foo.bar/04"), UrlStatus::Pending(vec![])),
1115 (PatchUrl::new("//foo.bar/0C"), UrlStatus::Pending(vec![])),
1116 ]),
1117 Some(cid_1()),
1118 Some(cid_2()),
1119 )
1120 .unwrap();
1121
1122 assert_eq!(
1123 group,
1124 CompatibleGroup::Mixed {
1125 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1126 "//foo.bar/04"
1127 ),)),
1128 iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
1129 }
1130 );
1131 assert!(preloads.is_empty());
1132 }
1133
1134 #[test]
1135 fn partial_invalidation_with_preloads() {
1136 let mut partial_c1 = partial(3, cid_1(), 9);
1137 let mut partial_c2 = partial(4, cid_2(), 9);
1138
1139 let expected_preloads_c1 = [PatchUrl::new("abc"), PatchUrl::new("def")];
1140 partial_c1.preload_urls.extend(expected_preloads_c1.clone());
1141
1142 let expected_preloads_c2 = [PatchUrl::new("hij"), PatchUrl::new("def")];
1143 partial_c2.preload_urls.extend(expected_preloads_c2.clone());
1144
1145 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1147 vec![partial_c1.clone()],
1148 &Default::default(),
1149 Some(cid_1()),
1150 Some(cid_2()),
1151 )
1152 .unwrap();
1153
1154 assert_eq!(
1155 group,
1156 CompatibleGroup::Mixed {
1157 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1158 "//foo.bar/0C"
1159 ),)),
1160 iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
1161 }
1162 );
1163 assert_eq!(preloads, BTreeSet::from(expected_preloads_c1.clone()));
1164
1165 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1167 vec![partial_c2.clone()],
1168 &Default::default(),
1169 Some(cid_1()),
1170 Some(cid_2()),
1171 )
1172 .unwrap();
1173
1174 assert_eq!(
1175 group,
1176 CompatibleGroup::Mixed {
1177 ift: ScopedGroup::NoInvalidation(BTreeMap::default()),
1178
1179 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1180 "//foo.bar/0G"
1181 ),)),
1182 }
1183 );
1184 assert_eq!(preloads, BTreeSet::from(expected_preloads_c2.clone()));
1185
1186 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1188 vec![partial_c1, partial_c2],
1189 &Default::default(),
1190 Some(cid_1()),
1191 Some(cid_2()),
1192 )
1193 .unwrap();
1194
1195 assert_eq!(
1196 group,
1197 CompatibleGroup::Mixed {
1198 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1199 "//foo.bar/0C"
1200 ),)),
1201 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1202 "//foo.bar/0G"
1203 ),)),
1204 }
1205 );
1206 assert_eq!(
1207 preloads,
1208 BTreeSet::from([
1209 PatchUrl::new("abc"),
1210 PatchUrl::new("def"),
1211 PatchUrl::new("hij")
1212 ])
1213 );
1214 }
1215
1216 #[test]
1217 fn partial_invalidation_with_preloads_removes_duplicates() {
1218 let mut partial_c1 = partial(3, cid_1(), 9);
1219 let mut partial_c2 = partial(4, cid_2(), 9);
1220
1221 let expected_preloads_c1 = ["abc".to_string(), "def".to_string()];
1222 partial_c1
1223 .preload_urls
1224 .extend(preload_list(&expected_preloads_c1));
1225 partial_c1.preload_urls.extend(preload_list(&[
1226 "//foo.bar/0C".to_string(),
1227 "//foo.bar/0G".to_string(),
1228 ]));
1229
1230 let expected_preloads_c2 = ["hij".to_string(), "def".to_string()];
1231 partial_c2
1232 .preload_urls
1233 .extend(preload_list(&expected_preloads_c2));
1234
1235 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1236 vec![partial_c1, partial_c2],
1237 &Default::default(),
1238 Some(cid_1()),
1239 Some(cid_2()),
1240 )
1241 .unwrap();
1242
1243 assert_eq!(
1244 group,
1245 CompatibleGroup::Mixed {
1246 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1247 "//foo.bar/0C"
1248 ),)),
1249 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1250 "//foo.bar/0G"
1251 ),)),
1252 }
1253 );
1254 assert_eq!(
1255 preloads,
1256 BTreeSet::from([
1257 PatchUrl::new("abc"),
1258 PatchUrl::new("def"),
1259 PatchUrl::new("hij"),
1260 ])
1261 );
1262 }
1263
1264 #[test]
1265 fn no_invalidation_with_preloads() {
1266 let expected_preloads_c1 = ["abc".to_string(), "def".to_string()];
1267 let expected_preloads_c2 = ["hij".to_string(), "def".to_string()];
1268 let mut p4_no_c1 = p4_no_c1();
1269 p4_no_c1
1270 .preload_urls
1271 .extend(preload_list(&expected_preloads_c1));
1272
1273 let mut p4_no_c2 = p4_no_c2();
1274 p4_no_c2
1275 .preload_urls
1276 .extend(preload_list(&expected_preloads_c1));
1277 let mut p5_no_c2 = p5_no_c2();
1278 p5_no_c2
1279 .preload_urls
1280 .extend(preload_list(&expected_preloads_c2));
1281
1282 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1284 vec![p4_no_c1, p5_no_c2.clone()],
1285 &Default::default(),
1286 Some(cid_1()),
1287 Some(cid_2()),
1288 )
1289 .unwrap();
1290
1291 assert_eq!(
1292 group,
1293 CompatibleGroup::Mixed {
1294 ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
1295 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1296 NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
1297 )])),
1298 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
1299 OrderedPatchUrl::url(0, "//foo.bar/0K"),
1300 NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
1301 )]))
1302 }
1303 );
1304 assert_eq!(
1305 preloads,
1306 BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
1307 );
1308
1309 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1311 vec![p4_no_c2, p5_no_c2],
1312 &Default::default(),
1313 Some(cid_1()),
1314 Some(cid_2()),
1315 )
1316 .unwrap();
1317
1318 assert_eq!(
1319 group,
1320 CompatibleGroup::Mixed {
1321 ift: ScopedGroup::NoInvalidation(Default::default()),
1322 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([
1323 (
1324 OrderedPatchUrl::url(0, "//foo.bar/0K"),
1325 NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
1326 ),
1327 (
1328 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1329 NoInvalidationPatch(patch_info_iftx("//foo.bar/0G"))
1330 )
1331 ]))
1332 }
1333 );
1334 assert_eq!(
1335 preloads,
1336 BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
1337 );
1338 }
1339
1340 #[test]
1341 fn no_invalidation_with_preloads_removes_duplicates() {
1342 let expected_preloads_c1 = ["abc".to_string(), "def".to_string()];
1343 let expected_preloads_c2 = ["hij".to_string(), "def".to_string()];
1344 let mut p4_no_c1 = p4_no_c1();
1345 p4_no_c1
1346 .preload_urls
1347 .extend(preload_list(&expected_preloads_c1));
1348
1349 let mut p5_no_c2 = p5_no_c2();
1350 p5_no_c2
1351 .preload_urls
1352 .extend(preload_list(&expected_preloads_c2));
1353 p5_no_c2.preload_urls.extend(preload_list(&[
1354 "//foo.bar/0G".to_string(),
1355 "//foo.bar/0K".to_string(),
1356 ]));
1357
1358 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1360 vec![p4_no_c1, p5_no_c2.clone()],
1361 &Default::default(),
1362 Some(cid_1()),
1363 Some(cid_2()),
1364 )
1365 .unwrap();
1366
1367 assert_eq!(
1368 group,
1369 CompatibleGroup::Mixed {
1370 ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
1371 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1372 NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
1373 )])),
1374 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
1375 OrderedPatchUrl::url(0, "//foo.bar/0K"),
1376 NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
1377 )]))
1378 }
1379 );
1380 assert_eq!(
1381 preloads,
1382 BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
1383 );
1384 }
1385
1386 #[test]
1387 fn mixed() {
1388 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1390 vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
1391 &Default::default(),
1392 Some(cid_1()),
1393 Some(cid_2()),
1394 )
1395 .unwrap();
1396
1397 assert_eq!(
1398 group,
1399 CompatibleGroup::Mixed {
1400 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1401 "//foo.bar/08"
1402 ),)),
1403 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
1404 OrderedPatchUrl::url(0, "//foo.bar/0K"),
1405 NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
1406 )]))
1407 }
1408 );
1409 assert!(preloads.is_empty());
1410
1411 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1413 vec![p3_partial_c2(), p4_no_c1(), p5_no_c2()],
1414 &Default::default(),
1415 Some(cid_1()),
1416 Some(cid_2()),
1417 )
1418 .unwrap();
1419
1420 assert_eq!(
1421 group,
1422 CompatibleGroup::Mixed {
1423 ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
1424 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1425 NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
1426 )])),
1427 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1428 "//foo.bar/0C"
1429 ),))
1430 }
1431 );
1432 assert!(preloads.is_empty());
1433
1434 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1436 vec![p2_partial_c1(), p4_no_c1()],
1437 &Default::default(),
1438 Some(cid_1()),
1439 Some(cid_2()),
1440 )
1441 .unwrap();
1442
1443 assert_eq!(
1444 group,
1445 CompatibleGroup::Mixed {
1446 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1447 "//foo.bar/08"
1448 ),)),
1449 iftx: ScopedGroup::NoInvalidation(BTreeMap::default()),
1450 }
1451 );
1452 assert!(preloads.is_empty());
1453
1454 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1456 vec![p3_partial_c2(), p5_no_c2()],
1457 &Default::default(),
1458 Some(cid_1()),
1459 Some(cid_2()),
1460 )
1461 .unwrap();
1462
1463 assert_eq!(
1464 group,
1465 CompatibleGroup::Mixed {
1466 ift: ScopedGroup::NoInvalidation(BTreeMap::default()),
1467 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1468 "//foo.bar/0C"
1469 ),)),
1470 }
1471 );
1472 assert!(preloads.is_empty());
1473 }
1474
1475 #[test]
1476 fn mixed_with_preloads() {
1477 let mut p2_partial_c1 = p2_partial_c1();
1478 p2_partial_c1
1479 .preload_urls
1480 .extend(preload_list(&["abc".to_string(), "def".to_string()]));
1481
1482 let mut p3_partial_c2 = p3_partial_c2();
1483 p3_partial_c2
1484 .preload_urls
1485 .extend(preload_list(&["klm".to_string(), "nop".to_string()]));
1486
1487 let mut p4_no_c1 = p4_no_c1();
1488 p4_no_c1
1489 .preload_urls
1490 .extend(preload_list(&["foo".to_string(), "bar".to_string()]));
1491
1492 let mut p5_no_c2 = p5_no_c2();
1493 p5_no_c2
1494 .preload_urls
1495 .extend(preload_list(&["hij".to_string(), "def".to_string()]));
1496
1497 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1499 vec![p2_partial_c1, p4_no_c1.clone(), p5_no_c2.clone()],
1500 &Default::default(),
1501 Some(cid_1()),
1502 Some(cid_2()),
1503 )
1504 .unwrap();
1505
1506 assert_eq!(
1507 group,
1508 CompatibleGroup::Mixed {
1509 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1510 "//foo.bar/08"
1511 ),)),
1512 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
1513 OrderedPatchUrl::url(0, "//foo.bar/0K"),
1514 NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
1515 )]))
1516 }
1517 );
1518 assert_eq!(
1519 preloads,
1520 BTreeSet::from(["abc", "def", "hij",].map(PatchUrl::new))
1521 );
1522
1523 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1525 vec![p3_partial_c2, p4_no_c1, p5_no_c2],
1526 &Default::default(),
1527 Some(cid_1()),
1528 Some(cid_2()),
1529 )
1530 .unwrap();
1531
1532 assert_eq!(
1533 group,
1534 CompatibleGroup::Mixed {
1535 ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
1536 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1537 NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
1538 )])),
1539 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1540 "//foo.bar/0C"
1541 ),))
1542 }
1543 );
1544 assert_eq!(
1545 preloads,
1546 BTreeSet::from(["klm", "nop", "foo", "bar",].map(PatchUrl::new))
1547 );
1548 }
1549
1550 #[test]
1551 fn missing_compat_ids() {
1552 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1554 vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
1555 &Default::default(),
1556 None,
1557 None,
1558 )
1559 .unwrap();
1560
1561 assert_eq!(
1562 group,
1563 CompatibleGroup::Mixed {
1564 ift: ScopedGroup::NoInvalidation(Default::default()),
1565 iftx: ScopedGroup::NoInvalidation(Default::default()),
1566 }
1567 );
1568 assert!(preloads.is_empty());
1569
1570 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1572 vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
1573 &Default::default(),
1574 Some(cid_1()),
1575 None,
1576 )
1577 .unwrap();
1578
1579 assert_eq!(
1580 group,
1581 CompatibleGroup::Mixed {
1582 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1583 "//foo.bar/08"
1584 ),)),
1585 iftx: ScopedGroup::NoInvalidation(Default::default()),
1586 }
1587 );
1588 assert!(preloads.is_empty());
1589
1590 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1592 vec![p2_partial_c1(), p4_no_c1(), p5_no_c2()],
1593 &Default::default(),
1594 None,
1595 Some(cid_1()),
1596 )
1597 .unwrap();
1598
1599 assert_eq!(
1600 group,
1601 CompatibleGroup::Mixed {
1602 ift: ScopedGroup::NoInvalidation(Default::default()),
1603 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1604 "//foo.bar/08"
1605 ),)),
1606 }
1607 );
1608 assert!(preloads.is_empty());
1609 }
1610
1611 #[test]
1612 fn dedups_urls() {
1613 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1615 vec![p4_no_c1(), p4_no_c1()],
1616 &Default::default(),
1617 Some(cid_1()),
1618 Some(cid_2()),
1619 )
1620 .unwrap();
1621
1622 assert_eq!(
1623 group,
1624 CompatibleGroup::Mixed {
1625 ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
1626 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1627 NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
1628 )])),
1629 iftx: ScopedGroup::NoInvalidation(BTreeMap::new()),
1630 }
1631 );
1632 assert!(preloads.is_empty());
1633
1634 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1636 vec![p4_no_c1(), p4_no_c2(), p5_no_c2()],
1637 &Default::default(),
1638 Some(cid_1()),
1639 Some(cid_2()),
1640 )
1641 .unwrap();
1642
1643 assert_eq!(
1644 group,
1645 CompatibleGroup::Mixed {
1646 ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
1647 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1648 NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
1649 )])),
1650 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
1651 OrderedPatchUrl::url(0, "//foo.bar/0K"),
1652 NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
1653 )])),
1654 }
1655 );
1656 assert!(preloads.is_empty());
1657
1658 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1660 vec![p2_partial_c1(), p2_partial_c2(), p3_partial_c2()],
1661 &Default::default(),
1662 Some(cid_1()),
1663 Some(cid_2()),
1664 )
1665 .unwrap();
1666
1667 assert_eq!(
1668 group,
1669 CompatibleGroup::Mixed {
1670 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1671 "//foo.bar/08"
1672 ))),
1673 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1674 "//foo.bar/0C"
1675 ))),
1676 }
1677 );
1678 assert!(preloads.is_empty());
1679
1680 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1682 vec![p2_partial_c1(), p2_no_c2(), p5_no_c2()],
1683 &Default::default(),
1684 Some(cid_1()),
1685 Some(cid_2()),
1686 )
1687 .unwrap();
1688
1689 assert_eq!(
1690 group,
1691 CompatibleGroup::Mixed {
1692 ift: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_ift(
1693 "//foo.bar/08"
1694 ))),
1695 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
1696 OrderedPatchUrl::url(0, "//foo.bar/0K"),
1697 NoInvalidationPatch(patch_info_iftx("//foo.bar/0K"))
1698 )])),
1699 }
1700 );
1701 assert!(preloads.is_empty());
1702
1703 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1704 vec![p3_partial_c2(), p3_no_c1(), p4_no_c1()],
1705 &Default::default(),
1706 Some(cid_1()),
1707 Some(cid_2()),
1708 )
1709 .unwrap();
1710
1711 assert_eq!(
1712 group,
1713 CompatibleGroup::Mixed {
1714 ift: ScopedGroup::NoInvalidation(BTreeMap::from([(
1715 OrderedPatchUrl::url(0, "//foo.bar/0G"),
1716 NoInvalidationPatch(patch_info_ift("//foo.bar/0G"))
1717 )])),
1718 iftx: ScopedGroup::PartialInvalidation(PartialInvalidationPatch(patch_info_iftx(
1719 "//foo.bar/0C"
1720 ))),
1721 }
1722 );
1723 assert!(preloads.is_empty());
1724 }
1725
1726 fn create_group_for(urls: Vec<PatchMapEntry>) -> PatchGroup<'static> {
1727 let data = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
1728 let (group, preloads) = PatchGroup::select_next_patches_from_candidates(
1729 urls,
1730 &Default::default(),
1731 Some(cid_1()),
1732 Some(cid_2()),
1733 )
1734 .unwrap();
1735
1736 PatchGroup {
1737 font: data,
1738 patches: Some(group),
1739 preload_urls: preloads,
1740 }
1741 }
1742
1743 fn empty_group() -> PatchGroup<'static> {
1744 let data = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
1745 PatchGroup {
1746 font: data,
1747 patches: None,
1748 preload_urls: Default::default(),
1749 }
1750 }
1751
1752 #[test]
1753 fn urls() {
1754 let g = create_group_for(vec![]);
1755 assert_eq!(
1756 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1757 Vec::<&str>::default()
1758 );
1759 assert!(!g.has_urls());
1760
1761 let g = empty_group();
1762 assert_eq!(
1763 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1764 Vec::<&str>::default()
1765 );
1766 assert!(!g.has_urls());
1767
1768 let g = create_group_for(vec![p1_full()]);
1769 assert_eq!(
1770 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1771 vec!["//foo.bar/04"],
1772 );
1773 assert!(g.has_urls());
1774
1775 let g = create_group_for(vec![p2_partial_c1(), p3_partial_c2()]);
1776 assert_eq!(
1777 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1778 vec!["//foo.bar/08", "//foo.bar/0C"]
1779 );
1780 assert!(g.has_urls());
1781
1782 let g = create_group_for(vec![p2_partial_c1()]);
1783 assert_eq!(
1784 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1785 vec!["//foo.bar/08",],
1786 );
1787 assert!(g.has_urls());
1788
1789 let g = create_group_for(vec![p3_partial_c2()]);
1790 assert_eq!(
1791 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1792 vec!["//foo.bar/0C"],
1793 );
1794 assert!(g.has_urls());
1795
1796 let g = create_group_for(vec![p2_partial_c1(), p4_no_c2(), p5_no_c2()]);
1797 assert_eq!(
1798 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1799 vec!["//foo.bar/08", "//foo.bar/0G", "//foo.bar/0K"],
1800 );
1801 assert!(g.has_urls());
1802
1803 let g = create_group_for(vec![p3_partial_c2(), p4_no_c1()]);
1804 assert_eq!(
1805 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1806 vec!["//foo.bar/0C", "//foo.bar/0G"],
1807 );
1808
1809 let g = create_group_for(vec![p4_no_c1(), p5_no_c2()]);
1810 assert_eq!(
1811 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1812 vec!["//foo.bar/0G", "//foo.bar/0K"],
1813 );
1814 assert!(g.has_urls());
1815 }
1816
1817 #[test]
1818 fn urls_with_preloads() {
1819 let mut p2_partial_c1 = p2_partial_c1();
1820 p2_partial_c1
1821 .preload_urls
1822 .extend(preload_list(&["abc".to_string(), "def".to_string()]));
1823
1824 let mut p3_partial_c2 = p3_partial_c2();
1825 p3_partial_c2
1826 .preload_urls
1827 .extend(preload_list(&["foo".to_string(), "bar".to_string()]));
1828
1829 let g = create_group_for(vec![p2_partial_c1, p3_partial_c2]);
1830 assert_eq!(
1831 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1832 vec!["//foo.bar/08", "//foo.bar/0C", "abc", "bar", "def", "foo"]
1833 );
1834 assert!(g.has_urls());
1835 }
1836
1837 #[test]
1838 fn select_next_patches_no_intersection() {
1839 let font = base_font(Some(table_keyed_format2()), None);
1840 let font = FontRef::new(&font).unwrap();
1841
1842 let s = SubsetDefinition::codepoints([55].into_iter().collect());
1843 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
1844
1845 assert!(!g.has_urls());
1846 assert_eq!(
1847 g.urls().map(|url| url.as_ref()).collect::<Vec<&str>>(),
1848 Vec::<&str>::default()
1849 );
1850
1851 assert_eq!(
1852 g.apply_next_patches(&mut Default::default()),
1853 Err(PatchingError::EmptyPatchList)
1854 );
1855 }
1856
1857 #[test]
1858 fn apply_patches_full_invalidation() {
1859 let font = base_font(Some(table_keyed_format2()), None);
1860 let font = FontRef::new(&font).unwrap();
1861
1862 let s = SubsetDefinition::codepoints([5].into_iter().collect());
1863 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
1864
1865 assert!(g.has_urls());
1866 let mut patch_data = HashMap::from([
1867 (
1868 PatchUrl::new("foo/04"),
1869 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
1870 ),
1871 (
1872 PatchUrl::new("foo/bar"),
1873 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
1874 ),
1875 ]);
1876
1877 let new_font = g.apply_next_patches(&mut patch_data).unwrap();
1878 let new_font = FontRef::new(&new_font).unwrap();
1879
1880 assert_eq!(
1881 new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
1882 TABLE_1_FINAL_STATE,
1883 );
1884 assert_eq!(
1885 new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
1886 TABLE_2_FINAL_STATE,
1887 );
1888
1889 assert_eq!(
1890 patch_data,
1891 HashMap::from([
1892 (PatchUrl::new("foo/04"), UrlStatus::Applied,),
1893 (
1894 PatchUrl::new("foo/bar"),
1895 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
1896 ),
1897 ])
1898 )
1899 }
1900
1901 struct CustomBrotliDecoder;
1902
1903 impl SharedBrotliDecoder for CustomBrotliDecoder {
1904 fn decode(
1905 &self,
1906 _encoded: &[u8],
1907 _shared_dictionary: Option<&[u8]>,
1908 _max_uncompressed_length: usize,
1909 ) -> Result<Vec<u8>, shared_brotli_patch_decoder::decode_error::DecodeError> {
1910 Ok(vec![1, 2, 3, 4, 5])
1911 }
1912 }
1913
1914 #[test]
1915 fn apply_patches_full_invalidation_with_custom_brotli() {
1916 let font = base_font(Some(table_keyed_format2()), None);
1917 let font = FontRef::new(&font).unwrap();
1918
1919 let s = SubsetDefinition::codepoints([5].into_iter().collect());
1920 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
1921
1922 assert!(g.has_urls());
1923 let mut patch_data = HashMap::from([
1924 (
1925 PatchUrl::new("foo/04"),
1926 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
1927 ),
1928 (
1929 PatchUrl::new("foo/bar"),
1930 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
1931 ),
1932 ]);
1933
1934 let new_font = g
1935 .apply_next_patches_with_decoder(&mut patch_data, &CustomBrotliDecoder)
1936 .unwrap();
1937 let new_font = FontRef::new(&new_font).unwrap();
1938
1939 assert_eq!(
1940 new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
1941 vec![1, 2, 3, 4, 5]
1942 );
1943 assert_eq!(
1944 new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
1945 vec![1, 2, 3, 4, 5]
1946 );
1947
1948 assert_eq!(
1949 patch_data,
1950 HashMap::from([
1951 (PatchUrl::new("foo/04"), UrlStatus::Applied,),
1952 (
1953 PatchUrl::new("foo/bar"),
1954 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
1955 ),
1956 ])
1957 )
1958 }
1959
1960 #[test]
1961 fn apply_patches_one_partial_invalidation() {
1962 let mut buffer = table_keyed_format2();
1963 buffer.write_at("encoding", 2u8);
1964
1965 let font = base_font(Some(buffer.clone()), None);
1967 let font = FontRef::new(&font).unwrap();
1968
1969 let s = SubsetDefinition::codepoints([5].into_iter().collect());
1970 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
1971
1972 let mut patch_data = HashMap::from([(
1973 PatchUrl::new("foo/04"),
1974 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
1975 )]);
1976
1977 let new_font = g.apply_next_patches(&mut patch_data).unwrap();
1978 let new_font = FontRef::new(&new_font).unwrap();
1979
1980 assert_eq!(
1981 new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
1982 TABLE_1_FINAL_STATE,
1983 );
1984 assert_eq!(
1985 new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
1986 TABLE_2_FINAL_STATE,
1987 );
1988
1989 assert_eq!(
1990 patch_data,
1991 HashMap::from([(PatchUrl::new("foo/04"), UrlStatus::Applied,),])
1992 );
1993
1994 let font = base_font(None, Some(buffer.clone()));
1996 let font = FontRef::new(&font).unwrap();
1997
1998 let s = SubsetDefinition::codepoints([5].into_iter().collect());
1999 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
2000
2001 let mut patch_data = HashMap::from([(
2002 PatchUrl::new("foo/04"),
2003 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
2004 )]);
2005
2006 let new_font = g.apply_next_patches(&mut patch_data).unwrap();
2007 let new_font = FontRef::new(&new_font).unwrap();
2008
2009 assert_eq!(
2010 new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
2011 TABLE_1_FINAL_STATE,
2012 );
2013 assert_eq!(
2014 new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
2015 TABLE_2_FINAL_STATE,
2016 );
2017
2018 assert_eq!(
2019 patch_data,
2020 HashMap::from([(PatchUrl::new("foo/04"), UrlStatus::Applied,),])
2021 );
2022 }
2023
2024 #[test]
2025 fn apply_patches_two_partial_invalidation() {
2026 let mut ift_buffer = table_keyed_format2();
2027 ift_buffer.write_at("encoding", 2u8);
2028
2029 let mut iftx_buffer = table_keyed_format2();
2030 iftx_buffer.write_at("compat_id[0]", 2u32);
2031 iftx_buffer.write_at("encoding", 2u8);
2032 iftx_buffer.write_at("id_delta", Int24::new(2)); let font = base_font(Some(ift_buffer), Some(iftx_buffer));
2035 let font = FontRef::new(&font).unwrap();
2036
2037 let s = SubsetDefinition::codepoints([5].into_iter().collect());
2038 let g = PatchGroup::select_next_patches(font.clone(), &Default::default(), &s).unwrap();
2039
2040 let mut patch_2 = table_keyed_patch();
2041 patch_2.write_at("compat_id", 2u32);
2042 patch_2.write_at("patch[0]", Tag::new(b"tab4"));
2043 patch_2.write_at("patch[1]", Tag::new(b"tab5"));
2044
2045 let mut patch_data = HashMap::from([
2046 (
2047 PatchUrl::new("foo/04"),
2048 UrlStatus::Pending(table_keyed_patch().as_slice().to_vec()),
2049 ),
2050 (
2051 PatchUrl::new("foo/08"),
2052 UrlStatus::Pending(patch_2.as_slice().to_vec()),
2053 ),
2054 ]);
2055
2056 let new_font = g.apply_next_patches(&mut patch_data).unwrap();
2057 let new_font = FontRef::new(&new_font).unwrap();
2058
2059 assert_eq!(
2060 new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
2061 TABLE_1_FINAL_STATE,
2062 );
2063 assert_eq!(
2064 new_font.table_data(Tag::new(b"tab2")).unwrap().as_bytes(),
2065 TABLE_2_FINAL_STATE,
2066 );
2067
2068 assert_eq!(
2070 new_font.table_data(Tag::new(b"tab4")).unwrap().as_bytes(),
2071 font.table_data(Tag::new(b"tab4")).unwrap().as_bytes(),
2072 );
2073 assert_eq!(
2074 new_font.table_data(Tag::new(b"tab5")).unwrap().as_bytes(),
2075 font.table_data(Tag::new(b"tab5")).unwrap().as_bytes(),
2076 );
2077 }
2078
2079 #[test]
2080 fn apply_patches_mixed() {
2081 let mut ift_builder = table_keyed_format2();
2082 ift_builder.write_at("encoding", 2u8);
2083
2084 let mut iftx_builder = table_keyed_format2();
2085 iftx_builder.write_at("encoding", 3u8);
2086 iftx_builder.write_at("compat_id[0]", 6u32);
2087 iftx_builder.write_at("compat_id[1]", 7u32);
2088 iftx_builder.write_at("compat_id[2]", 8u32);
2089 iftx_builder.write_at("compat_id[3]", 9u32);
2090 iftx_builder.write_at("id_delta", Int24::new(2)); let font = test_font_for_patching_with_loca_mod(
2093 true,
2094 |_| {},
2095 HashMap::from([
2096 (IFT_TAG, ift_builder.as_slice()),
2097 (IFTX_TAG, iftx_builder.as_slice()),
2098 (Tag::new(b"tab1"), "abcdef\n".as_bytes()),
2099 ]),
2100 );
2101 let font = FontRef::new(font.as_slice()).unwrap();
2102
2103 let s = SubsetDefinition::codepoints([5].into_iter().collect());
2104 let g = PatchGroup::select_next_patches(font.clone(), &Default::default(), &s).unwrap();
2105
2106 let patch_ift = table_keyed_patch();
2107 let patch_iftx =
2108 assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
2109
2110 let mut patch_data = HashMap::from([
2111 (
2112 PatchUrl::new("foo/04"),
2113 UrlStatus::Pending(patch_ift.as_slice().to_vec()),
2114 ),
2115 (
2116 PatchUrl::new("foo/08"),
2117 UrlStatus::Pending(patch_iftx.as_slice().to_vec()),
2118 ),
2119 ]);
2120
2121 let new_font = g.apply_next_patches(&mut patch_data).unwrap();
2122 let new_font = FontRef::new(&new_font).unwrap();
2123
2124 assert_eq!(
2125 new_font.table_data(Tag::new(b"tab1")).unwrap().as_bytes(),
2126 TABLE_1_FINAL_STATE,
2127 );
2128
2129 assert_eq!(
2131 new_font.table_data(Tag::new(b"glyf")).unwrap().as_bytes(),
2132 font.table_data(Tag::new(b"glyf")).unwrap().as_bytes(),
2133 );
2134 }
2135
2136 #[test]
2137 fn apply_patches_all_no_invalidation() {
2138 let mut ift_builder = table_keyed_format2();
2139 ift_builder.write_at("encoding", 3u8);
2140 ift_builder.write_at("compat_id[0]", 6u32);
2141 ift_builder.write_at("compat_id[1]", 7u32);
2142 ift_builder.write_at("compat_id[2]", 8u32);
2143 ift_builder.write_at("compat_id[3]", 9u32);
2144
2145 let mut iftx_builder = table_keyed_format2();
2146 iftx_builder.write_at("encoding", 3u8);
2147 iftx_builder.write_at("compat_id[0]", 7u32);
2148 iftx_builder.write_at("compat_id[1]", 7u32);
2149 iftx_builder.write_at("compat_id[2]", 8u32);
2150 iftx_builder.write_at("compat_id[3]", 9u32);
2151 iftx_builder.write_at("id_delta", Int24::new(2)); let font = test_font_for_patching_with_loca_mod(
2154 true,
2155 |_| {},
2156 HashMap::from([
2157 (IFT_TAG, ift_builder.as_slice()),
2158 (IFTX_TAG, iftx_builder.as_slice()),
2159 ]),
2160 );
2161
2162 let font = FontRef::new(font.as_slice()).unwrap();
2163
2164 let s = SubsetDefinition::codepoints([5].into_iter().collect());
2165 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
2166
2167 let patch1 =
2168 assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
2169
2170 let mut patch2 = glyf_u16_glyph_patches();
2171 patch2.write_at("gid_13", 14u16);
2172 let mut header = glyph_keyed_patch_header();
2173 header.write_at("compatibility_id", 7u32);
2174 let patch2 = assemble_glyph_keyed_patch(header, patch2);
2175
2176 let mut patch_data = HashMap::from([
2177 (
2178 PatchUrl::new("foo/04"),
2179 UrlStatus::Pending(patch1.as_slice().to_vec()),
2180 ),
2181 (
2182 PatchUrl::new("foo/08"),
2183 UrlStatus::Pending(patch2.as_slice().to_vec()),
2184 ),
2185 ]);
2186
2187 let new_font = g.apply_next_patches(&mut patch_data).unwrap();
2188 let new_font = FontRef::new(&new_font).unwrap();
2189
2190 let new_glyf: &[u8] = new_font.table_data(Tag::new(b"glyf")).unwrap().as_bytes();
2191 assert_eq!(
2192 &[
2193 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', ],
2201 new_glyf
2202 );
2203
2204 assert_eq!(
2205 patch_data,
2206 HashMap::from([
2207 (PatchUrl::new("foo/04"), UrlStatus::Applied,),
2208 (PatchUrl::new("foo/08"), UrlStatus::Applied,),
2209 ])
2210 );
2211
2212 let g = PatchGroup::select_next_patches(new_font, &Default::default(), &s).unwrap();
2214 assert!(!g.has_urls());
2215 }
2216
2217 #[test]
2218 fn apply_patches_no_invalidation_duplicate_urls() {
2219 let mut ift_builder = table_keyed_format2();
2223 ift_builder.write_at("encoding", 3u8);
2224 ift_builder.write_at("compat_id[0]", 6u32);
2225 ift_builder.write_at("compat_id[1]", 7u32);
2226 ift_builder.write_at("compat_id[2]", 8u32);
2227 ift_builder.write_at("compat_id[3]", 9u32);
2228 ift_builder.write_at("entry_count", Uint24::new(2));
2229
2230 let ift_builder = ift_builder
2231 .push(0b00100100u8) .push(Int24::new(-2)) .push(100u16) .extend([0b00001101, 0b00000011, 0b00110001u8]);
2236
2237 let mut iftx_builder = table_keyed_format2();
2238 iftx_builder.write_at("encoding", 3u8);
2239 iftx_builder.write_at("compat_id[0]", 0u32);
2240 iftx_builder.write_at("compat_id[1]", 0u32);
2241 iftx_builder.write_at("compat_id[2]", 0u32);
2242 iftx_builder.write_at("compat_id[3]", 2u32);
2243 iftx_builder.write_at("bias", 100u16);
2244
2245 let font = test_font_for_patching_with_loca_mod(
2253 true,
2254 |_| {},
2255 HashMap::from([
2256 (IFT_TAG, ift_builder.as_slice()),
2257 (IFTX_TAG, iftx_builder.as_slice()),
2258 ]),
2259 );
2260
2261 let font = FontRef::new(font.as_slice()).unwrap();
2262
2263 let s = SubsetDefinition::codepoints([5].into_iter().collect());
2264 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
2265
2266 let patch =
2267 assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
2268
2269 let mut patch_data = HashMap::from([(
2270 PatchUrl::new("foo/04"),
2271 UrlStatus::Pending(patch.as_slice().to_vec()),
2272 )]);
2273
2274 let new_font = g.apply_next_patches(&mut patch_data).unwrap();
2275 let new_font = FontRef::new(&new_font).unwrap();
2276
2277 let new_glyf: &[u8] = new_font.table_data(Tag::new(b"glyf")).unwrap().as_bytes();
2278 assert_eq!(
2279 &[
2280 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', ],
2287 new_glyf
2288 );
2289
2290 assert_eq!(
2291 patch_data,
2292 HashMap::from([(PatchUrl::new("foo/04"), UrlStatus::Applied,),])
2293 );
2294
2295 let all = SubsetDefinition::all();
2297 let group = PatchGroup::select_next_patches(new_font, &Default::default(), &all).unwrap();
2298 let mut info = patch_info_iftx("foo/04");
2299 info.application_flag_bit_indices.clear();
2300 info.application_flag_bit_indices.insert(334);
2301 assert_eq!(
2302 group.patches.unwrap(),
2303 CompatibleGroup::Mixed {
2304 ift: ScopedGroup::NoInvalidation(Default::default()),
2305 iftx: ScopedGroup::NoInvalidation(BTreeMap::from([(
2306 OrderedPatchUrl::url(0, "foo/04"),
2307 NoInvalidationPatch(info)
2308 )])),
2309 }
2310 );
2311 }
2312
2313 #[test]
2314 fn tables_have_same_compat_id() {
2315 let ift_buffer = table_keyed_format2();
2316 let iftx_buffer = table_keyed_format2();
2317
2318 let font = base_font(Some(ift_buffer), Some(iftx_buffer));
2319 let font = FontRef::new(&font).unwrap();
2320
2321 let s = SubsetDefinition::codepoints([5].into_iter().collect());
2322 let g = PatchGroup::select_next_patches(font.clone(), &Default::default(), &s);
2323
2324 assert!(g.is_err(), "did not fail as expected.");
2325 if let Err(err) = g {
2326 assert_eq!(ReadError::ValidationError, err);
2327 }
2328 }
2329
2330 #[test]
2331 fn invalid_url_templates() {
2332 let mut buffer = table_keyed_format2();
2333 buffer.write_at("url_template_var_end", b'~');
2334
2335 let font = base_font(Some(buffer), None);
2336 let font = FontRef::new(&font).unwrap();
2337
2338 let s = SubsetDefinition::codepoints([5].into_iter().collect());
2339
2340 let Err(err) = PatchGroup::select_next_patches(font, &Default::default(), &s) else {
2341 panic!("Should have failed")
2342 };
2343 assert_eq!(
2344 err,
2345 ReadError::MalformedData("Failed to expand url template in format 2 table.")
2346 );
2347 }
2348
2349 #[test]
2350 fn select_next_patches_ordering_for_non_invalidating() {
2351 let mut ift_builder = custom_ids_format2();
2352 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();
2356 iftx_builder.write_at("compat_id[0]", 5u32);
2357 iftx_builder.write_at("compat_id[1]", 6u32);
2358 iftx_builder.write_at("compat_id[2]", 7u32);
2359 iftx_builder.write_at("compat_id[3]", 8u32);
2360
2361 let font = test_font_for_patching_with_loca_mod(
2362 true,
2363 |_| {},
2364 HashMap::from([
2365 (IFT_TAG, ift_builder.as_slice()),
2366 (IFTX_TAG, iftx_builder.as_slice()),
2367 ]),
2368 );
2369
2370 let font = FontRef::new(font.as_slice()).unwrap();
2371
2372 let s = SubsetDefinition::codepoints([10].into_iter().collect());
2373 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
2374
2375 let urls: Vec<String> = g.urls().map(|p| p.as_ref().to_string()).collect();
2386 let expected_urls: Vec<String> = [16, 12, 21, 0, 6, 15]
2387 .into_iter()
2388 .map(|index| PatchUrl::expand_template(RELATIVE_URL_TEMPLATE, &PatchId::Numeric(index)))
2389 .map(|url| url.unwrap())
2390 .map(|url| url.as_ref().to_string())
2391 .collect();
2392
2393 assert_eq!(urls, expected_urls);
2394 }
2395
2396 #[test]
2397 fn select_next_patches_ordering_for_non_invalidating_with_duplicates() {
2398 let mut ift_builder = custom_ids_format2();
2399 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();
2403 iftx_builder.write_at("compat_id[0]", 5u32);
2404 iftx_builder.write_at("compat_id[1]", 6u32);
2405 iftx_builder.write_at("compat_id[2]", 7u32);
2406 iftx_builder.write_at("compat_id[3]", 8u32);
2407
2408 let font = test_font_for_patching_with_loca_mod(
2409 true,
2410 |_| {},
2411 HashMap::from([
2412 (IFT_TAG, ift_builder.as_slice()),
2413 (IFTX_TAG, iftx_builder.as_slice()),
2414 ]),
2415 );
2416
2417 let font = FontRef::new(font.as_slice()).unwrap();
2418
2419 let s = SubsetDefinition::codepoints([10].into_iter().collect());
2420 let g = PatchGroup::select_next_patches(font, &Default::default(), &s).unwrap();
2421
2422 let urls: Vec<String> = g.urls().map(|p| p.as_ref().to_string()).collect();
2433 let expected_urls: Vec<String> = [16, 7, 0, 6, 15]
2434 .into_iter()
2435 .map(|index| PatchUrl::expand_template(RELATIVE_URL_TEMPLATE, &PatchId::Numeric(index)))
2436 .map(|url| url.unwrap())
2437 .map(|url| url.as_ref().to_string())
2438 .collect();
2439
2440 assert_eq!(urls, expected_urls);
2441 }
2442}