Skip to main content

incremental_font_transfer/
patch_group.rs

1//! API for selecting and applying a group of IFT patches.
2//!
3//! This provides methods for selecting a maximal group of patches that are compatible with each other and
4//! additionally methods for applying that group of patches.
5
6use 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/// A group of patches derived from a single IFT font.
24///
25/// This is a group which can be applied simultaneously to that font. Patches are
26/// initially missing data which must be fetched and supplied to patch application
27/// method.
28///
29/// Also optionally includes a list of patches which are not compatible but have been
30/// requested to be preloaded.
31#[derive(Clone)]
32pub struct PatchGroup<'a> {
33    font: FontRef<'a>,
34    patches: Option<CompatibleGroup>,
35
36    // These patches aren't compatible, but have been requested as preloads by the
37    // patch mapping.
38    preload_urls: BTreeSet<PatchUrl>,
39}
40
41// We implement Debug manually because FontRef does not implement Debug unconditionally.
42// Additionally, FontRef may be too verbose to be helpful.
43impl 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    /// Intersect the available and unapplied patches in ift_font against subset_definition
81    ///
82    /// patch_data provides any patch data that has been previously loaded, keyed by patch url.
83    /// May be empty if no patch data is loaded yet.
84    ///
85    /// Returns a group of patches which would be applied next.
86    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            // The spec disallows two tables with same compat ids.
104            // See: https://w3c.github.io/IFT/Overview.html#extend-font-subset
105            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    /// Returns an iterator over URLs in this group.
123    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    /// Returns true if there is at least one url associated with this group.
131    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        // Some notes about this implementation:
199        // - From candidates we need to form the largest possible group of patches which follow the selection criteria
200        //   from: https://w3c.github.io/IFT/Overview.html#extend-font-subset and won't invalidate each other.
201        //
202        // - Validation constraints are encoded into the structure of CompatibleGroup so the task here is to fill up
203        //   a compatible group appropriately.
204        //
205        // - When multiple valid choices exist the specification provides a procedure for picking amongst the options:
206        //   https://w3c.github.io/IFT/Overview.html#invalidating-patch-selection
207        //
208        // - During selection we need to ensure that there are no PatchInfo's with duplicate URLs. The spec doesn't
209        //   require erroring on this case, and it's resolved by:
210        //   - In the spec algo patches are selected and applied one at a time.
211        //   - Further it specifically disallows re-applying the same URL later.
212        //   - So therefore we de-dup by retaining the particular instance which has the highest selection
213        //     priority.
214
215        // Step 1: sort the candidates into separate lists based on invalidation characteristics.
216        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        // First check for a full invalidation patch, if one exists it's the only selection possible
232        if let Some(patch) = Self::select_invalidating_candidate(full_invalidation) {
233            // TODO(garretrieger): use a heuristic to select the best patch
234            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        // Otherwise fill in the two possible selections in priority order (as defined by `Selection`):
240        // 1. Partial Invalidating IFT
241        // 2. Partial Invalidating IFTX
242        // 3. Non Invalidating IFT
243        // 4. Non Invalidating IFT
244        // The selections are stored in scoped_groups
245        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                    // Select a partial invalidating candidate if possible
258                    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        // Remove any url's selected above from the preloads
297        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    /// Select an entry from a list of candidate invalidating entries according to the specs selection criteria.
337    ///
338    /// Context: <https://w3c.github.io/IFT/Overview.html#invalidating-patch-selection>
339    fn select_invalidating_candidate<T>(candidates: T) -> Option<CandidatePatch>
340    where
341        T: IntoIterator<Item = CandidatePatch>,
342    {
343        // Note:
344        // - As mentioned in the spec we can find at least one entry matching that criteria by finding an entry with the
345        //   largest intersection (since that can't be a strict subset of others).
346        // - Intersection size is tracked in intersection info.
347        // - Ties are broken by entry order, which is also tracked in intersection info.
348        // - So it's sufficient to just find a candidate patch with the largest intersection info, relying on it's
349        //   Ord implementation.
350        candidates.into_iter().max()
351    }
352
353    /// Attempt to apply the next patch (or patches if non-invalidating) listed in this group.
354    ///
355    /// Returns the bytes of the updated font.
356    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    /// Attempt to apply the next patch (or patches if non-invalidating) listed in this group.
364    ///
365    /// Returns the bytes of the updated font.
366    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 => {} // previously applied urls are ignored according to the spec.
385            }
386        }
387
388        // No invalidating patches left, so apply any non invalidating ones in one pass.
389        // First check if we have all of the needed data.
390        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 => {} // previously applied urls are ignored according to the spec.
400                }
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    // TODO(garretrieger): do we need sorted order, use HashMap instead?
427    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            // TODO(garretrieger): for efficiency can we delay url template resolution until we have actually selected patches?
442            // TODO(garretrieger): for btree construction don't recompute the resolved url, cache inside the patch url object?
443            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                                // When duplicate URLs are present we want to always keep the one with the
475                                // lowest entry order.
476                                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/// Tracks whether a URL has already been applied to a font or not.
491#[derive(PartialEq, Eq, Debug, Clone)]
492pub enum UrlStatus {
493    Applied,
494    Pending(Vec<u8>),
495}
496
497/// Tracks information related to a patch necessary to apply that patch.
498#[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/// Type to track a patch being considered for selection.
530#[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        // Ordering is primarily derived from intersection info, but first we need to check
573        // if either patch is already loaded. If so the loaded one is prioritized above any
574        // that are not already loaded.
575        //
576        // See: https://w3c.github.io/IFT/Overview.html#invalidating-patch-selection
577        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/// Type for a single non invalidating patch.
599#[derive(PartialEq, Eq, Debug, Clone)]
600struct NoInvalidationPatch(PatchInfo);
601
602/// Type for a single partially invalidating patch.
603#[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/// Type for a single fully invalidating patch.
613#[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/// Represents a group of patches which are valid (compatible) to be applied together to
623/// an IFT font.
624#[derive(PartialEq, Eq, Debug, Clone)]
625enum CompatibleGroup {
626    Full(FullInvalidationPatch),
627    Mixed { ift: ScopedGroup, iftx: ScopedGroup },
628}
629
630/// A set of zero or more compatible patches that are derived from the same scope
631/// ("IFT " vs "IFTX")
632#[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/// For non invalidating patches the specification requires they be ordered by entry order.
657///
658/// That is the order that the physical entries in the patch map are in. This struct
659/// adds that ordering onto PatchUrl's for when they are stored in a btree set/map.
660/// Context: <https://github.com/w3c/IFT/pull/279>
661#[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        // Only IFT
996        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        // Only IFTX
1020        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        // Both
1045        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        // 1 -> 04
1077        // 2 -> 08
1078        // 3 -> 0C
1079        // //foo.bar/{id}
1080
1081        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            // Entry 3 is marked as already loaded which gives it priority
1088            &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        // With multiple preloaded
1106        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            // Entry 1 and 3 are marked as already loaded which gives it priority
1113            &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        // Only IFT
1146        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        // Only IFTX
1166        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        // Both
1187        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        // (no inval, no inval)
1283        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        // (None, no inval)
1310        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        // (no inval, no inval)
1359        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        // (partial, no inval)
1389        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        // (no inval, partial)
1412        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        // (partial, empty)
1435        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        // (empty, partial)
1455        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        // (partial, no inval)
1498        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        // (no inval, partial)
1524        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        // (None, None)
1553        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        // (Some, None)
1571        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        // (None, Some)
1591        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        // Duplicates inside a scope
1614        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        // Duplicates across scopes (no invalidation + no invalidation)
1635        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        // Duplicates across scopes (partial + partial)
1659        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        // Duplicates across scopes (partial + no invalidation)
1681        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        // IFT
1966        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        // IFTX
1995        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)); // delta = +1
2033
2034        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        // only the first patch gets applied so tab4/tab5 are unchanged.
2069        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)); // delta = +1
2091
2092        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        // only the partial invalidation patch gets applied, so glyf is unchanged.
2130        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)); // delta = 1
2152
2153        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, // gid 0
2194                6, 7, 8, 0, // gid 1
2195                b'a', b'b', b'c', 0, // gid2
2196                b'd', b'e', b'f', b'g', // gid 7
2197                b'h', b'i', b'j', b'k', b'l', 0, // gid 8 + 9
2198                b'm', b'n', // gid 13
2199                b'm', b'n', // gid 14
2200            ],
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        // there should be no more applicable patches left now.
2213        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        // Two types of duplicate url situations
2220        // 1. Same mapping table has duplicate urls. All should be marked applied.
2221        // 2. Different mapping table has duplicate urls. These will not be marked as applied.
2222        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) // format
2232            .push(Int24::new(-2)) // id delta
2233            .push(100u16) // bias
2234            // codpeoints {100..117}
2235            .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        // Total mapping is:
2246        // IFT:
2247        // {0..17} -> foo/04
2248        // {100..117} -> foo/04
2249        // IFTX:
2250        // {100..117} -> foo/04
2251
2252        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, // gid 0
2281                6, 7, 8, 0, // gid 1
2282                b'a', b'b', b'c', 0, // gid2
2283                b'd', b'e', b'f', b'g', // gid 7
2284                b'h', b'i', b'j', b'k', b'l', 0, // gid 8 + 9
2285                b'm', b'n', // gid 13
2286            ],
2287            new_glyf
2288        );
2289
2290        assert_eq!(
2291            patch_data,
2292            HashMap::from([(PatchUrl::new("foo/04"), UrlStatus::Applied,),])
2293        );
2294
2295        // there should be one IFTX patch for foo/04 left now.
2296        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)); // delta = +15
2353        ift_builder.write_at("entries[1].id_delta", Int24::new(-10)); // delta = -5
2354
2355        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        // Expected ID ordering
2376        // IFT
2377        // 16 (+15)
2378        // 12 (-5)
2379        // 21 (+7, +0)
2380        // IFTX
2381        // 0
2382        // 6
2383        // 15
2384        // Note: these are in entry order (physical ordering in the map encoding) and not in entry id order
2385        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)); // delta = +15
2400        ift_builder.write_at("entries[1].id_delta", Int24::new(-20)); // delta = -10
2401
2402        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        // Expected ID ordering
2423        // IFT
2424        // 16 (+15)
2425        // 7 (-10)
2426        // 16 (+7, +0)
2427        // IFTX
2428        // 0
2429        // 6
2430        // 15
2431        // Note: these are in entry order (physical ordering in the map encoding) and not in entry id order
2432        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}