Repository: googlefonts/fontations
Pull request #1005 (2024-07-17 05:45:42)

ci: Update to `actions/checkout` v4 from v3
This helps to address a deprecation notice within the GitHub Actions UI.


Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 2fb1c1a3..88eb5470 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -15,7 +15,7 @@ jobs:
     name: Rustfmt
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: dtolnay/rust-toolchain@stable
       - run: rustup component add rustfmt
 
@@ -34,7 +34,7 @@ jobs:
     name: Clippy lints
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: install stable toolchain
         uses: dtolnay/rust-toolchain@stable
 
@@ -48,7 +48,7 @@ jobs:
     name: cargo test stable
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: install stable toolchain
         uses: dtolnay/rust-toolchain@stable
@@ -80,7 +80,7 @@ jobs:
     name: check codegen is up-to-date
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: install stable toolchain
         uses: dtolnay/rust-toolchain@stable
@@ -97,7 +97,7 @@ jobs:
     name: cargo check no std
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: install stable toolchain
         uses: dtolnay/rust-toolchain@stable
@@ -115,7 +115,7 @@ jobs:
     name: cargo check wasm
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: install stable toolchain
         uses: dtolnay/rust-toolchain@stable
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 2301e589..fc41536d 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -14,6 +14,7 @@ release = false
 [dependencies]
 libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
 skrifa = { path="../skrifa" }
+int-set = { path = "../int-set" }
 
 [[bin]]
 name = "fuzz_skrifa_charmap"
@@ -43,4 +44,10 @@ doc = false
 name = "fuzz_skrifa_color"
 path = "fuzz_targets/fuzz_skrifa_color.rs"
 test = false
-doc = false
\ No newline at end of file
+doc = false
+
+[[bin]]
+name = "fuzz_int_set"
+path = "fuzz_targets/fuzz_int_set.rs"
+test = false
+doc = false
diff --git a/fuzz/fuzz_targets/fuzz_int_set.rs b/fuzz/fuzz_targets/fuzz_int_set.rs
new file mode 100644
index 00000000..e10aefe1
--- /dev/null
+++ b/fuzz/fuzz_targets/fuzz_int_set.rs
@@ -0,0 +1,673 @@
+#![no_main]
+//! A correctness fuzzer that checks all of the public API methods of IntSet.
+//!
+//! This fuzzer exercises the public API of IntSet and compares the results to the same operations run
+//! on a BTreeSet. Any differences in behaviour and/or set contents triggers a panic.
+//!
+//! The fuzzer input data is interpretted as a series of op codes which map to the public api methods of IntSet.
+//!
+//! Note: currently only inclusive mode IntSet's are tested.
+
+use std::cmp::max;
+use std::ops::Bound::Excluded;
+use std::ops::Bound::Included;
+use std::ops::RangeInclusive;
+use std::{collections::BTreeSet, error::Error};
+
+use int_set::IntSet;
+use libfuzzer_sys::fuzz_target;
+
+const OPERATION_COUNT: usize = 7_500;
+
+// TODO(garretrieger): allow inverted sets to be accessed.
+// TODO(garretrieger): allow a limited domain set to be accessed.
+
+struct Input<'a> {
+    // The state includes 2 of each type of sets to allow us to test out binary set operations (eg. union)
+    int_set: &'a mut IntSet<u32>,
+    btree_set: &'a mut BTreeSet<u32>,
+}
+
+impl Input<'_> {
+    fn from<'a>(int_set: &'a mut IntSet<u32>, btree_set: &'a mut BTreeSet<u32>) -> Input<'a> {
+        Input { int_set, btree_set }
+    }
+}
+
+trait Operation {
+    fn size(&self, set_len: usize) -> usize;
+    fn operate(&self, input: Input, other: Input);
+}
+
+/* ### Insert ### */
+
+struct InsertOp(u32);
+
+impl InsertOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(val), data) = read_u32(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(val))), data)
+    }
+}
+
+impl Operation for InsertOp {
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.insert(self.0);
+        input.btree_set.insert(self.0);
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length.ilog2() as usize
+    }
+}
+
+/* ### Insert Range ### */
+
+struct InsertRangeOp(u32, u32);
+
+impl InsertRangeOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(min), data) = read_u32(data) else {
+            return (None, data);
+        };
+        let (Some(max), data) = read_u32(data) else {
+            return (None, data);
+        };
+
+        (Some(Box::new(Self(min, max))), data)
+    }
+}
+
+impl Operation for InsertRangeOp {
+    fn size(&self, length: usize) -> usize {
+        if self.1 < self.0 {
+            return 1;
+        }
+        ((self.1 as usize - self.0 as usize) + 1) * (length.ilog2() as usize)
+    }
+
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.insert_range(self.0..=self.1);
+        for v in self.0..=self.1 {
+            input.btree_set.insert(v);
+        }
+    }
+}
+
+/* ### Remove ### */
+
+struct RemoveOp(u32);
+
+impl RemoveOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(val), data) = read_u32(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(val))), data)
+    }
+}
+
+impl Operation for RemoveOp {
+    fn size(&self, length: usize) -> usize {
+        length.ilog2() as usize
+    }
+
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.remove(self.0);
+        input.btree_set.remove(&self.0);
+    }
+}
+
+/* ### Remove Range ### */
+
+struct RemoveRangeOp(u32, u32);
+
+impl RemoveRangeOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(min), data) = read_u32(data) else {
+            return (None, data);
+        };
+        let (Some(max), data) = read_u32(data) else {
+            return (None, data);
+        };
+
+        (Some(Box::new(Self(min, max))), data)
+    }
+}
+
+impl Operation for RemoveRangeOp {
+    fn size(&self, length: usize) -> usize {
+        if self.1 < self.0 {
+            return 1;
+        }
+        ((self.1 as usize - self.0 as usize) + 1) * (length.ilog2() as usize)
+    }
+
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.remove_range(self.0..=self.1);
+        for v in self.0..=self.1 {
+            input.btree_set.remove(&v);
+        }
+    }
+}
+
+/* ### Length ### */
+struct LengthOp();
+
+impl LengthOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for LengthOp {
+    fn operate(&self, input: Input, _: Input) {
+        assert_eq!(input.int_set.len(), input.btree_set.len());
+    }
+
+    fn size(&self, _: usize) -> usize {
+        1
+    }
+}
+
+/* ### Is Empty ### */
+
+struct IsEmptyOp();
+
+impl IsEmptyOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for IsEmptyOp {
+    fn operate(&self, input: Input, _: Input) {
+        assert_eq!(input.int_set.is_empty(), input.btree_set.is_empty());
+    }
+
+    fn size(&self, _: usize) -> usize {
+        1
+    }
+}
+
+/* ### Contains ### */
+struct ContainsOp(u32);
+
+impl ContainsOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(val), data) = read_u32(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(val))), data)
+    }
+}
+
+impl Operation for ContainsOp {
+    fn operate(&self, input: Input, _: Input) {
+        assert_eq!(
+            input.int_set.contains(self.0),
+            input.btree_set.contains(&self.0)
+        );
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length.ilog2() as usize
+    }
+}
+
+/* ### Clear  ### */
+
+struct ClearOp();
+
+impl ClearOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for ClearOp {
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.clear();
+        input.btree_set.clear();
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length
+    }
+}
+
+/* ### Intersects Range ### */
+
+struct IntersectsRangeOp(u32, u32);
+
+impl IntersectsRangeOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(min), data) = read_u32(data) else {
+            return (None, data);
+        };
+        let (Some(max), data) = read_u32(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(min, max))), data)
+    }
+}
+
+impl Operation for IntersectsRangeOp {
+    fn operate(&self, input: Input, _: Input) {
+        let int_set_intersects = input.int_set.intersects_range(self.0..=self.1);
+
+        let mut btree_intersects = false;
+        for v in self.0..=self.1 {
+            if input.btree_set.contains(&v) {
+                btree_intersects = true;
+                break;
+            }
+        }
+
+        assert_eq!(int_set_intersects, btree_intersects);
+    }
+
+    fn size(&self, length: usize) -> usize {
+        if self.1 < self.0 {
+            return 1;
+        }
+        ((self.1 as usize - self.0 as usize) + 1) * (length.ilog2() as usize)
+    }
+}
+
+/* ### First  ### */
+
+struct FirstOp();
+
+impl FirstOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for FirstOp {
+    fn operate(&self, input: Input, _: Input) {
+        assert_eq!(input.int_set.first(), input.btree_set.first().copied());
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length.ilog2() as usize
+    }
+}
+
+/* ### First  ### */
+
+struct LastOp();
+
+impl LastOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for LastOp {
+    fn operate(&self, input: Input, _: Input) {
+        assert_eq!(input.int_set.last(), input.btree_set.last().copied());
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length.ilog2() as usize
+    }
+}
+
+/* ### Iter ### */
+
+struct IterOp();
+
+impl IterOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for IterOp {
+    fn operate(&self, input: Input, _: Input) {
+        assert!(input.int_set.iter().eq(input.btree_set.iter().copied()));
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length
+    }
+}
+
+/* ### Iter Ranges ### */
+
+struct IterRangesOp();
+
+impl IterRangesOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for IterRangesOp {
+    fn operate(&self, input: Input, _: Input) {
+        let mut btree_ranges: Vec<RangeInclusive<u32>> = vec![];
+        let mut cur_range: Option<RangeInclusive<u32>> = None;
+
+        for v in input.btree_set.iter().copied() {
+            if let Some(range) = cur_range {
+                if range.end() + 1 == v {
+                    cur_range = Some(*range.start()..=v);
+                    continue;
+                }
+                btree_ranges.push(range);
+            }
+
+            cur_range = Some(v..=v);
+        }
+
+        if let Some(range) = cur_range {
+            btree_ranges.push(range);
+        }
+
+        assert!(input.int_set.iter_ranges().eq(btree_ranges.iter().cloned()));
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length
+    }
+}
+
+/* ### Iter After ### */
+
+struct IterAfterOp(u32);
+
+impl IterAfterOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(val), data) = read_u32(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(val))), data)
+    }
+}
+
+impl Operation for IterAfterOp {
+    fn operate(&self, input: Input, _: Input) {
+        let it = input.int_set.iter_after(self.0);
+        let btree_it = input
+            .btree_set
+            .range((Excluded(self.0), Included(u32::MAX)));
+        assert!(it.eq(btree_it.copied()));
+    }
+
+    fn size(&self, length: usize) -> usize {
+        length
+    }
+}
+
+/* ### Remove All ### */
+
+struct RemoveAllOp(Vec<u32>);
+
+impl RemoveAllOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(values), data) = read_u32_vec(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(values))), data)
+    }
+}
+
+impl Operation for RemoveAllOp {
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.remove_all(self.0.iter().copied());
+        for v in self.0.iter() {
+            input.btree_set.remove(v);
+        }
+    }
+
+    fn size(&self, length: usize) -> usize {
+        (length.ilog2() as usize) * self.0.len()
+    }
+}
+
+/* ### Extend ### */
+
+struct ExtendOp(Vec<u32>);
+
+impl ExtendOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(values), data) = read_u32_vec(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(values))), data)
+    }
+}
+
+impl Operation for ExtendOp {
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.extend(self.0.iter().copied());
+        input.btree_set.extend(self.0.iter().copied());
+    }
+
+    fn size(&self, length: usize) -> usize {
+        (length.ilog2() as usize) * self.0.len()
+    }
+}
+
+/* ### Extend Unsorted ### */
+
+struct ExtendUnsortedOp(Vec<u32>);
+
+impl ExtendUnsortedOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        let (Some(values), data) = read_u32_vec(data) else {
+            return (None, data);
+        };
+        (Some(Box::new(Self(values))), data)
+    }
+}
+
+impl Operation for ExtendUnsortedOp {
+    fn operate(&self, input: Input, _: Input) {
+        input.int_set.extend_unsorted(self.0.iter().copied());
+        input.btree_set.extend(self.0.iter().copied());
+    }
+
+    fn size(&self, length: usize) -> usize {
+        (length.ilog2() as usize) * self.0.len()
+    }
+}
+
+/* ### Union ### */
+
+struct UnionOp();
+
+impl UnionOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for UnionOp {
+    fn operate(&self, a: Input, b: Input) {
+        a.int_set.union(b.int_set);
+        for v in b.btree_set.iter() {
+            a.btree_set.insert(*v);
+        }
+    }
+
+    fn size(&self, length: usize) -> usize {
+        // TODO(garretrieger): should be length a + length b
+        length
+    }
+}
+
+/* ### Intersect ### */
+
+struct IntersectOp();
+
+impl IntersectOp {
+    fn parse_args(data: &[u8]) -> (Option<Box<dyn Operation>>, &[u8]) {
+        (Some(Box::new(Self())), data)
+    }
+}
+
+impl Operation for IntersectOp {
+    fn operate(&self, a: Input, b: Input) {
+        a.int_set.intersect(b.int_set);
+        let mut intersected: BTreeSet<u32> =
+            a.btree_set.intersection(b.btree_set).copied().collect();
+        std::mem::swap(a.btree_set, &mut intersected);
+    }
+
+    fn size(&self, length: usize) -> usize {
+        // TODO(garretrieger): should be length a + length b
+        length
+    }
+}
+
+/* ### End of Ops ### */
+
+fn read_u8(data: &[u8]) -> (Option<u8>, &[u8]) {
+    if data.is_empty() {
+        return (None, data);
+    }
+    (Some(data[0]), &data[1..])
+}
+
+fn read_u32(data: &[u8]) -> (Option<u32>, &[u8]) {
+    if data.len() < 4 {
+        return (None, data);
+    }
+    (
+        Some(
+            ((data[0] as u32) << 24)
+                | ((data[1] as u32) << 16)
+                | ((data[2] as u32) << 8)
+                | (data[3] as u32),
+        ),
+        &data[4..],
+    )
+}
+
+fn read_u32_vec(data: &[u8]) -> (Option<Vec<u32>>, &[u8]) {
+    let (Some(count), data) = read_u8(data) else {
+        return (None, data);
+    };
+
+    let mut values: Vec<u32> = vec![];
+    let mut data = data;
+    for _ in 0..count {
+        let r = read_u32(data);
+        let Some(value) = r.0 else {
+            return (None, data);
+        };
+        data = r.1;
+        values.push(value);
+    }
+    (Some(values), data)
+}
+
+struct NextOperation<'a> {
+    op: Box<dyn Operation>,
+    set_index: usize,
+    data: &'a [u8],
+}
+
+fn next_operation(data: &[u8]) -> Option<NextOperation> {
+    let op_code = data.first()?;
+
+    // Check the msb of op code to see which set index to use.
+    const INDEX_MASK: u8 = 0b10000000;
+    let set_index = if (op_code & INDEX_MASK) > 0 { 1 } else { 0 };
+    let op_code = !INDEX_MASK & op_code;
+
+    // TODO ops for most public api methods (have operations for iter() be what checks for
+    //      iter() equality alongside the check at end):
+    // - invert
+    // - inclusive_iter
+    // - is_inverted
+    let data = &data[1..];
+    let (op, data) = match op_code {
+        1 => InsertOp::parse_args(data),
+        2 => RemoveOp::parse_args(data),
+        3 => InsertRangeOp::parse_args(data),
+        4 => RemoveRangeOp::parse_args(data),
+        5 => LengthOp::parse_args(data),
+        6 => IsEmptyOp::parse_args(data),
+        7 => ContainsOp::parse_args(data),
+        8 => ClearOp::parse_args(data),
+        9 => IntersectsRangeOp::parse_args(data),
+        10 => FirstOp::parse_args(data),
+        11 => LastOp::parse_args(data),
+        12 => IterOp::parse_args(data),
+        13 => IterRangesOp::parse_args(data),
+        14 => IterAfterOp::parse_args(data),
+        15 => RemoveAllOp::parse_args(data),
+        16 => ExtendOp::parse_args(data),
+        17 => ExtendUnsortedOp::parse_args(data),
+        18 => UnionOp::parse_args(data),
+        19 => IntersectOp::parse_args(data),
+        _ => (None, data),
+    };
+
+    let op = op?;
+    Some(NextOperation {
+        op,
+        set_index,
+        data,
+    })
+}
+
+fn process_op_codes(data: &[u8]) -> Result<(), Box<dyn Error>> {
+    let mut int_set_0 = IntSet::<u32>::empty();
+    let mut int_set_1 = IntSet::<u32>::empty();
+    let mut btree_set_0 = BTreeSet::<u32>::new();
+    let mut btree_set_1 = BTreeSet::<u32>::new();
+
+    let mut ops = 0usize;
+    let mut data = data;
+    loop {
+        let next_op = next_operation(data);
+        let Some(next_op) = next_op else {
+            break;
+        };
+
+        data = next_op.data;
+
+        {
+            let btree_set = if next_op.set_index == 0 {
+                &btree_set_0
+            } else {
+                &btree_set_1
+            };
+            // when computing size use minimum length of 2 to ensure minimum value of log2(length) is 1.
+            ops = ops.saturating_add(next_op.op.size(max(2, btree_set.len())));
+            if ops > OPERATION_COUNT {
+                // Operation count limit reached.
+                break;
+            }
+        }
+
+        let (input, other) = if next_op.set_index == 0 {
+            (
+                Input::from(&mut int_set_0, &mut btree_set_0),
+                Input::from(&mut int_set_1, &mut btree_set_1),
+            )
+        } else {
+            (
+                Input::from(&mut int_set_1, &mut btree_set_1),
+                Input::from(&mut int_set_0, &mut btree_set_0),
+            )
+        };
+
+        next_op.op.operate(input, other);
+    }
+
+    assert!(int_set_0.iter().eq(btree_set_0.iter().copied()));
+    assert!(int_set_1.iter().eq(btree_set_1.iter().copied()));
+    Ok(())
+}
+
+fuzz_target!(|data: &[u8]| {
+    let _ = process_op_codes(data);
+});
diff --git a/int-set/src/bitset.rs b/int-set/src/bitset.rs
index a37af534..fba5eaf6 100644
--- a/int-set/src/bitset.rs
+++ b/int-set/src/bitset.rs
@@ -467,7 +467,8 @@ impl BitSet {
     }
 
     fn major_end(&self, major: u32) -> u32 {
-        self.major_start(major) + PAGE_BITS - 1
+        // Note: (PAGE_BITS - 1) must be grouped to prevent overflow on addition for the largest page.
+        self.major_start(major) + (PAGE_BITS - 1)
     }
 
     /// Returns the index in self.pages (if it exists) for the page with the same major as major_value.
@@ -690,11 +691,10 @@ impl<'a> Iterator for BitSetRangeIter<'a> {
 
     fn next(&mut self) -> Option<Self::Item> {
         self.page_iter.as_ref()?;
-
         let mut current_range = self.next_range();
         loop {
             let page = self.set.page_map.get(self.page_info_index)?;
-            let page_end = self.set.major_end(page.index);
+            let page_end = self.set.major_end(page.major_value);
 
             let Some(range) = current_range.clone() else {
                 // The current page has no more ranges, but there may be more pages.
@@ -779,7 +779,6 @@ mod test {
     fn iter_ranges() {
         check_iter_ranges(vec![0..=0]);
         check_iter_ranges(vec![4578..=4578]);
-
         check_iter_ranges(vec![0..=10, 4578..=4583]);
         check_iter_ranges(vec![0..=700]);
         check_iter_ranges(vec![353..=737]);
@@ -790,6 +789,8 @@ mod test {
 
         check_iter_ranges(vec![0..=511, 513..=517]);
         check_iter_ranges(vec![512..=1023, 1025..=1027]);
+
+        check_iter_ranges(vec![1792..=2650]);
     }
 
     #[test]
diff --git a/read-fonts/README.md b/read-fonts/README.md
index d1bb66b0..f3a2597b 100644
--- a/read-fonts/README.md
+++ b/read-fonts/README.md
@@ -14,7 +14,7 @@ of the library.
 Much of the code in this crate is generated automatically. This generated code
 lives in the `generated` directory. Each file in this directory is included
 in a module in the `src` directory, using Rust's [`include!`] macro. This allows
-us to seperate the generated code from any custom implementation code, while
+us to separate the generated code from any custom implementation code, while
 allowing them to exist in the same module.
 
 ### what we generate
@@ -38,7 +38,7 @@ presence of fields the existence of which may depend on the table's version.
 
 ### variable lengths and version-dependent fields
 
-**n.b**: *the design described below has not been benchmarked agaisnt the
+**n.b**: *the design described below has not been benchmarked against the
 alternatives, and may change*
 
 For fields that have variable length, or which only exist in certain table
diff --git a/read-fonts/src/read.rs b/read-fonts/src/read.rs
index e71a4455..8ecf6cbc 100644
--- a/read-fonts/src/read.rs
+++ b/read-fonts/src/read.rs
@@ -13,7 +13,7 @@ use crate::font_data::FontData;
 /// underlying bytes. (Tables that require external state implement
 /// [`FontReadWithArgs`] instead)
 pub trait FontRead<'a>: Sized {
-    /// Read an instace of `Self` from the provided data, performing validation.
+    /// Read an instance of `Self` from the provided data, performing validation.
     ///
     /// In the case of a table, this method is responsible for ensuring the input
     /// data is consistent: this means ensuring that any versioned fields are
@@ -24,7 +24,7 @@ pub trait FontRead<'a>: Sized {
 
 //NOTE: this is separate so that it can be a super trait of FontReadWithArgs and
 //ComputeSize, without them needing to know about each other? I'm not sure this
-//is necessary, but I don't know the full heirarchy of traits I'm going to need
+//is necessary, but I don't know the full hierarchy of traits I'm going to need
 //yet, so this seems... okay?
 
 /// A trait for a type that needs additional arguments to be read.
@@ -126,7 +126,7 @@ impl std::fmt::Display for ReadError {
             ReadError::InvalidArrayLen => {
                 write!(f, "Specified array length not a multiple of item size")
             }
-            ReadError::ValidationError => write!(f, "A validation error occured"),
+            ReadError::ValidationError => write!(f, "A validation error occurred"),
             ReadError::NullOffset => write!(f, "An offset was unexpectedly null"),
             ReadError::TableIsMissing(tag) => write!(f, "the {tag} table is missing"),
             ReadError::MetricIsMissing(tag) => write!(f, "the {tag} metric is missing"),
diff --git a/read-fonts/src/tables/cmap.rs b/read-fonts/src/tables/cmap.rs
index 8b67591b..a0e62451 100644
--- a/read-fonts/src/tables/cmap.rs
+++ b/read-fonts/src/tables/cmap.rs
@@ -587,7 +587,7 @@ mod tests {
     }
 
     // Make sure we don't bail early when iterating ranges with holes.
-    // Encounted with Gentium Basic and Gentium Basic Book.
+    // Encountered with Gentium Basic and Gentium Basic Book.
     // See <https://github.com/googlefonts/fontations/issues/897>
     #[test]
     fn cmap4_iter_sparse_range() {
diff --git a/read-fonts/src/tables/glyf.rs b/read-fonts/src/tables/glyf.rs
index 82e102e2..025f1bba 100644
--- a/read-fonts/src/tables/glyf.rs
+++ b/read-fonts/src/tables/glyf.rs
@@ -271,9 +271,9 @@ impl<'a> SimpleGlyph<'a> {
 /// This type is a simpler representation of the data in the blob.
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct CurvePoint {
-    /// X cooordinate.
+    /// X coordinate.
     pub x: i16,
-    /// Y cooordinate.
+    /// Y coordinate.
     pub y: i16,
     /// True if this is an on-curve point.
     pub on_curve: bool,
@@ -688,7 +688,7 @@ impl fmt::Display for ToPathError {
         match self {
             Self::ContourOrder(ix) => write!(
                 f,
-                "Contour end point at index {ix} was less than preceeding end point"
+                "Contour end point at index {ix} was less than preceding end point"
             ),
             Self::ExpectedQuad(ix) => write!(f, "Expected quadatic off-curve point at index {ix}"),
             Self::ExpectedQuadOrOnCurve(ix) => write!(
diff --git a/read-fonts/src/tables/gsub/closure.rs b/read-fonts/src/tables/gsub/closure.rs
index 55dfd246..8a4b77a8 100644
--- a/read-fonts/src/tables/gsub/closure.rs
+++ b/read-fonts/src/tables/gsub/closure.rs
@@ -29,7 +29,7 @@ pub(crate) trait GlyphClosure {
 }
 
 impl<'a> Gsub<'a> {
-    /// Return the set of glyphs reachable from the input set via any substituion.
+    /// Return the set of glyphs reachable from the input set via any substitution.
     pub fn closure_glyphs(
         &self,
         mut glyphs: HashSet<GlyphId16>,
diff --git a/read-fonts/src/tables/hmtx.rs b/read-fonts/src/tables/hmtx.rs
index a4469d6b..ec48b241 100644
--- a/read-fonts/src/tables/hmtx.rs
+++ b/read-fonts/src/tables/hmtx.rs
@@ -8,7 +8,7 @@ impl<'a> Hmtx<'a> {
         advance(self.h_metrics(), glyph_id)
     }
 
-    /// Returns the left side bearing for the given glyph identifer.
+    /// Returns the left side bearing for the given glyph identifier.
     pub fn side_bearing(&self, glyph_id: GlyphId) -> Option<i16> {
         side_bearing(self.h_metrics(), self.left_side_bearings(), glyph_id)
     }
diff --git a/read-fonts/src/tables/postscript/blend.rs b/read-fonts/src/tables/postscript/blend.rs
index 51b03740..7fcaba90 100644
--- a/read-fonts/src/tables/postscript/blend.rs
+++ b/read-fonts/src/tables/postscript/blend.rs
@@ -92,8 +92,8 @@ impl<'a> BlendState<'a> {
         self.data = None;
         self.region_indices = &[];
         let store = &self.store;
-        let varation_data = store.item_variation_data();
-        let data = varation_data
+        let variation_data = store.item_variation_data();
+        let data = variation_data
             .get(self.store_index as usize)
             .ok_or(Error::InvalidVariationStoreIndex(self.store_index))??;
         let region_indices = data.region_indexes();
diff --git a/read-fonts/src/tables/postscript/string.rs b/read-fonts/src/tables/postscript/string.rs
index 1e31092a..5bcfb877 100644
--- a/read-fonts/src/tables/postscript/string.rs
+++ b/read-fonts/src/tables/postscript/string.rs
@@ -17,7 +17,7 @@ impl StringId {
 
     /// Resolves the identifier as a standard string.
     ///
-    /// If the identifer represents a standard string, returns `Ok(string)`,
+    /// If the identifier represents a standard string, returns `Ok(string)`,
     /// otherwise returns `Err(index)` with the index that should be used to
     /// retrieve the string from the CFF string INDEX.
     ///
@@ -26,7 +26,7 @@ impl StringId {
     pub fn standard_string(self) -> Result<Latin1String<'static>, usize> {
         let ix = self.0 as usize;
         if let Some(string) = STANDARD_STRINGS.get(ix) {
-            // The standard strings are all ASCII so it's safe to intepret them
+            // The standard strings are all ASCII so it's safe to interpret them
             // as Latin-1. This is verified in a unit test.
             Ok(Latin1String::new(string.as_bytes()))
         } else {
diff --git a/read-fonts/src/tables/vmtx.rs b/read-fonts/src/tables/vmtx.rs
index b6737145..e48abc2b 100644
--- a/read-fonts/src/tables/vmtx.rs
+++ b/read-fonts/src/tables/vmtx.rs
@@ -11,7 +11,7 @@ impl<'a> Vmtx<'a> {
         hmtx::advance(self.v_metrics(), glyph_id)
     }
 
-    /// Returns the top side bearing for the given glyph identifer.
+    /// Returns the top side bearing for the given glyph identifier.
     pub fn side_bearing(&self, glyph_id: GlyphId) -> Option<i16> {
         hmtx::side_bearing(self.v_metrics(), self.top_side_bearings(), glyph_id)
     }
diff --git a/skrifa/src/instance.rs b/skrifa/src/instance.rs
index c5a79e40..8a17f37b 100644
--- a/skrifa/src/instance.rs
+++ b/skrifa/src/instance.rs
@@ -14,7 +14,7 @@ pub type NormalizedCoord = read_fonts::types::F2Dot14;
 /// in CSS (assuming a DPI scale factor of 1.0).
 ///
 /// To retrieve metrics and outlines in font units, use the [unscaled](Self::unscaled)
-/// construtor on this type.
+/// constructor on this type.
 #[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
 pub struct Size(Option<f32>);
 
diff --git a/skrifa/src/metrics.rs b/skrifa/src/metrics.rs
index f65df908..80e3f4c2 100644
--- a/skrifa/src/metrics.rs
+++ b/skrifa/src/metrics.rs
@@ -16,7 +16,7 @@
 //! to select the desired instance of a font:
 //! * Size: represented by the [Size] type, this determines the scaling factor that is
 //! applied to all metrics.
-//! * Normalized variation coordinates: respresented by the [LocationRef] type,
+//! * Normalized variation coordinates: represented by the [LocationRef] type,
 //! these define the position in design space for a variable font. For a non-variable
 //! font, these coordinates are ignored and you can pass [LocationRef::default()]
 //! as an argument for this parameter.
diff --git a/skrifa/src/outline/cff/hint.rs b/skrifa/src/outline/cff/hint.rs
index 002910b4..a84ab09c 100644
--- a/skrifa/src/outline/cff/hint.rs
+++ b/skrifa/src/outline/cff/hint.rs
@@ -81,7 +81,7 @@ pub(crate) struct HintState {
     blue_shift: Fixed,
     blue_fuzz: Fixed,
     language_group: i32,
-    supress_overshoot: bool,
+    suppress_overshoot: bool,
     do_em_box_hints: bool,
     boost: Fixed,
     darken_y: Fixed,
@@ -97,7 +97,7 @@ impl HintState {
             blue_shift: params.blue_shift,
             blue_fuzz: params.blue_fuzz,
             language_group: params.language_group,
-            supress_overshoot: false,
+            suppress_overshoot: false,
             do_em_box_hints: false,
             boost: Fixed::ZERO,
             darken_y: Fixed::ZERO,
@@ -242,7 +242,7 @@ impl HintState {
         }
         // Suppress overshoot and boost blue zones at small sizes
         if self.scale < self.blue_scale {
-            self.supress_overshoot = true;
+            self.suppress_overshoot = true;
             self.boost =
                 Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale);
             // boost must remain less than 0.5, or baseline could go negative
@@ -277,7 +277,7 @@ impl HintState {
                 && bottom_edge.cs_coord <= (zone.cs_top_edge + fuzz)
             {
                 // Bottom edge captured by bottom zone.
-                adjustment = if self.supress_overshoot {
+                adjustment = if self.suppress_overshoot {
                     zone.ds_flat_edge
                 } else if zone.cs_top_edge - bottom_edge.cs_coord >= self.blue_shift {
                     // Guarantee minimum of 1 pixel overshoot
@@ -298,7 +298,7 @@ impl HintState {
                 && top_edge.cs_coord <= (zone.cs_top_edge + fuzz)
             {
                 // Top edge captured by top zone.
-                adjustment = if self.supress_overshoot {
+                adjustment = if self.suppress_overshoot {
                     zone.ds_flat_edge
                 } else if top_edge.cs_coord - zone.cs_bottom_edge >= self.blue_shift {
                     // Guarantee minimum of 1 pixel overshoot
@@ -1137,7 +1137,7 @@ mod tests {
         assert!(!state.do_em_box_hints);
         assert_eq!(state.zone_count, 6);
         assert_eq!(state.boost, Fixed::from_bits(27035));
-        assert!(state.supress_overshoot);
+        assert!(state.suppress_overshoot);
         // FreeType generates the following zones:
         let expected_zones = &[
             // csBottomEdge	-983040	int
diff --git a/skrifa/src/outline/cff/mod.rs b/skrifa/src/outline/cff/mod.rs
index d5e90e00..7264ce09 100644
--- a/skrifa/src/outline/cff/mod.rs
+++ b/skrifa/src/outline/cff/mod.rs
@@ -47,7 +47,7 @@ pub(crate) struct Outlines<'a> {
 impl<'a> Outlines<'a> {
     /// Creates a new scaler for the given font.
     ///
-    /// This will choose an underyling CFF2 or CFF table from the font, in that
+    /// This will choose an underlying CFF2 or CFF table from the font, in that
     /// order.
     pub fn new(font: &impl TableProvider<'a>) -> Result<Self, Error> {
         let units_per_em = font.head()?.units_per_em();
@@ -467,7 +467,7 @@ impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> {
     }
 }
 
-/// Command sink adapter that supresses degenerate move and line commands.
+/// Command sink adapter that suppresses degenerate move and line commands.
 ///
 /// FreeType avoids emitting empty contours and zero length lines to prevent
 /// artifacts when stem darkening is enabled. We don't support stem darkening
diff --git a/skrifa/src/outline/error.rs b/skrifa/src/outline/error.rs
index 3778fc63..42846516 100644
--- a/skrifa/src/outline/error.rs
+++ b/skrifa/src/outline/error.rs
@@ -21,7 +21,7 @@ pub enum DrawError {
     InsufficientMemory,
     /// Exceeded a recursion limit when loading a glyph.
     RecursionLimitExceeded(GlyphId),
-    /// Error occured during hinting.
+    /// Error occurred during hinting.
     HintingFailed(HintError),
     /// An anchor point had invalid indices.
     InvalidAnchorPoint(GlyphId, u16),
@@ -29,7 +29,7 @@ pub enum DrawError {
     PostScript(CffError),
     /// Conversion from outline to path failed.
     ToPath(ToPathError),
-    /// Error occured when reading font data.
+    /// Error occurred when reading font data.
     Read(ReadError),
     /// HarfBuzz style drawing with hints is not supported
     // Error rather than silently returning unhinted per f2f discussion.
diff --git a/skrifa/src/outline/glyf/hint/engine/logical.rs b/skrifa/src/outline/glyf/hint/engine/logical.rs
index 1015a1f3..a51cbf0b 100644
--- a/skrifa/src/outline/glyf/hint/engine/logical.rs
+++ b/skrifa/src/outline/glyf/hint/engine/logical.rs
@@ -42,7 +42,7 @@ impl<'a> Engine<'a> {
         self.value_stack.apply_binary(|a, b| Ok((a <= b) as i32))
     }
 
-    /// Greather than.
+    /// Greater than.
     ///
     /// GT[] (0x52)
     ///
diff --git a/skrifa/src/outline/glyf/hint/error.rs b/skrifa/src/outline/glyf/hint/error.rs
index 925c8c96..9610a551 100644
--- a/skrifa/src/outline/glyf/hint/error.rs
+++ b/skrifa/src/outline/glyf/hint/error.rs
@@ -46,7 +46,7 @@ impl core::fmt::Display for HintErrorKind {
             Self::NestedDefinition => write!(f, "nested function or instruction definition"),
             Self::DefinitionTooLarge => write!(
                 f,
-                "function or instruction definition exceeded the maxmium size of 64k"
+                "function or instruction definition exceeded the maximum size of 64k"
             ),
             Self::TooManyDefinitions => write!(f, "too many function or instruction definitions"),
             Self::InvalidDefinition(key) => {
diff --git a/skrifa/src/outline/glyf/hint/graphics.rs b/skrifa/src/outline/glyf/hint/graphics.rs
index 46b5ba42..9046f54c 100644
--- a/skrifa/src/outline/glyf/hint/graphics.rs
+++ b/skrifa/src/outline/glyf/hint/graphics.rs
@@ -22,7 +22,7 @@ pub enum CoordAxis {
 /// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html>
 #[derive(Debug)]
 pub struct GraphicsState<'a> {
-    /// Fields of the graphics state that persist between calls to the intepreter.
+    /// Fields of the graphics state that persist between calls to the interpreter.
     pub retained: RetainedGraphicsState,
     /// A unit vector whose direction establishes an axis along which
     /// distances are measured.
@@ -88,7 +88,7 @@ pub struct GraphicsState<'a> {
     pub zones: [Zone<'a>; 2],
     /// True if the current glyph is a composite.
     pub is_composite: bool,
-    /// If true, enables a set of backward compabitility heuristics that
+    /// If true, enables a set of backward compatibility heuristics that
     /// prevent certain modifications to the outline. The purpose is to
     /// support "modern" vertical only hinting that attempts to preserve
     /// outline shape and metrics in the horizontal direction. This is
diff --git a/skrifa/src/outline/glyf/hint/zone.rs b/skrifa/src/outline/glyf/hint/zone.rs
index 7631450e..bc11c515 100644
--- a/skrifa/src/outline/glyf/hint/zone.rs
+++ b/skrifa/src/outline/glyf/hint/zone.rs
@@ -421,7 +421,7 @@ impl GraphicsState<'_> {
         distance: F26Dot6,
     ) -> Result<(), HintErrorKind> {
         // Note: we never adjust x in backward compatibility mode and we never
-        // adjust y in backward compability mode after IUP has been done in
+        // adjust y in backward compatibility mode after IUP has been done in
         // both directions.
         //
         // The primary motivation is to avoid horizontal adjustments in cases
diff --git a/skrifa/src/outline/glyf/outline.rs b/skrifa/src/outline/glyf/outline.rs
index 579cc56e..ea52ee46 100644
--- a/skrifa/src/outline/glyf/outline.rs
+++ b/skrifa/src/outline/glyf/outline.rs
@@ -38,7 +38,7 @@ pub struct Outline<'a> {
     /// for each component. This tracks the maximum stack depth necessary
     /// to store those values during processing.
     pub max_component_delta_stack: usize,
-    /// Number of entires in the hinting value stack.
+    /// Number of entries in the hinting value stack.
     pub max_stack: usize,
     /// Number of CVT entries for copy-on-write support.
     pub cvt_count: usize,
diff --git a/skrifa/src/outline/mod.rs b/skrifa/src/outline/mod.rs
index fc97d401..ddc4a698 100644
--- a/skrifa/src/outline/mod.rs
+++ b/skrifa/src/outline/mod.rs
@@ -1067,7 +1067,7 @@ mod tests {
         let bez = Affine::FLIP_Y * pen.path; // like an icon svg
         let actual_path_start = &bez.elements()[..expected_path_start.len()];
         // round2 can still leave very small differences from the typed 2-decimal value
-        // and the diff isn't pleasent so just compare as svg string fragments
+        // and the diff isn't pleasant so just compare as svg string fragments
         assert_eq!(
             svg_commands(expected_path_start),
             svg_commands(actual_path_start)
diff --git a/skrifa/src/setting.rs b/skrifa/src/setting.rs
index aead4d9c..338c8466 100644
--- a/skrifa/src/setting.rs
+++ b/skrifa/src/setting.rs
@@ -83,7 +83,7 @@ impl<T: Copy> From<&(&str, T)> for Setting<T> {
 /// The `selector` field should contain a tag that corresponds to a
 /// variation axis while the `value` field specifies the desired position
 /// on the axis in user coordinates (i.e. within the range defined by
-/// the mininum and maximum values of the axis).
+/// the minimum and maximum values of the axis).
 ///
 /// # Example
 /// ```
diff --git a/skrifa/src/string.rs b/skrifa/src/string.rs
index 627d05da..0b3d221c 100644
--- a/skrifa/src/string.rs
+++ b/skrifa/src/string.rs
@@ -372,7 +372,7 @@ const LANGUAGE_ID_TO_BCP47: &[(u16, &str)] = &[
     /* A mapping from Windows LCID to BCP 47 codes.
      *  This list is the sorted, curated output of tools/win_lcid.cpp.
      *  Note that these are sorted by value for quick binary lookup, and not logically by lsb.
-     *  The 'bare' language ids (e.g. 0x0001 for Arabic) are ommitted
+     *  The 'bare' language ids (e.g. 0x0001 for Arabic) are omitted
      *  as they do not appear as valid language ids in the OpenType specification.
      */
     (0x0401, "ar-SA"),        //Arabic
diff --git a/skrifa/src/variation.rs b/skrifa/src/variation.rs
index 7872dd8a..4d7c3a0d 100644
--- a/skrifa/src/variation.rs
+++ b/skrifa/src/variation.rs
@@ -394,7 +394,7 @@ impl<'a> NamedInstanceCollection<'a> {
         })
     }
 
-    /// Returns an iterator over the instances in the colletion.
+    /// Returns an iterator over the instances in the collection.
     pub fn iter(&self) -> impl Iterator<Item = NamedInstance<'a>> + 'a + Clone {
         let copy = self.clone();
         (0..self.len()).filter_map(move |i| copy.get(i))


Test patch:
diff --git a/skrifa/src/color/traversal_tests/mod.rs b/skrifa/src/color/traversal_tests/mod.rs
index 6dc5a266..be3b675d 100644
--- a/skrifa/src/color/traversal_tests/mod.rs
+++ b/skrifa/src/color/traversal_tests/mod.rs
@@ -253,7 +253,7 @@ fn should_rebaseline() -> bool {
     env::var("REBASELINE_COLRV1_TESTS").is_ok()
 }
 
-// To regenerate the baselines, set the enviroment variable `REBASELINE_COLRV1_TESTS`
+// To regenerate the baselines, set the environment variable `REBASELINE_COLRV1_TESTS`
 // when running tests, for example like this:
 // $ REBASELINE_COLRV1_TESTS=1 cargo test color::traversal
 fn colrv1_traversal_test(