From 2a2aa74c09538b1b09d9273aa0c9eaea288ad2dd Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 18:44:28 -0700 Subject: [PATCH 01/18] fix(context): add context finalization iteration limit This prevents any possibility of an infinite loop during finalization. --- src/actions.rs | 7 +++++-- src/context.rs | 20 ++++++++++++++++---- src/main.rs | 5 ++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/actions.rs b/src/actions.rs index 3e0c039..2893d9f 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,5 +1,5 @@ use crate::context::SproutContext; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use std::rc::Rc; @@ -50,7 +50,10 @@ pub fn execute(context: Rc, name: impl AsRef) -> Result<()> bail!("unknown action '{}'", name.as_ref()); }; // Finalize the context and freeze it. - let context = context.finalize().freeze(); + let context = context + .finalize() + .context("unable to finalize context")? + .freeze(); // Execute the action. if let Some(chainload) = &action.chainload { diff --git a/src/context.rs b/src/context.rs index 8755e83..fbcbc80 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,11 +1,14 @@ use crate::actions::ActionDeclaration; use crate::options::SproutOptions; -use anyhow::Result; use anyhow::anyhow; +use anyhow::{Result, bail}; use std::collections::{BTreeMap, BTreeSet}; use std::rc::Rc; use uefi::proto::device_path::DevicePath; +/// The maximum number of iterations that can be performed in [SproutContext::finalize]. +const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100; + /// Declares a root context for Sprout. /// This contains data that needs to be shared across Sprout. #[derive(Default)] @@ -151,11 +154,20 @@ impl SproutContext { /// Finalizes a context by producing a context with no parent that contains all the values /// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no /// inheritance with other [SproutContext]s. It will still contain a [RootContext] however. - pub fn finalize(&self) -> SproutContext { + pub fn finalize(&self) -> Result { // Collect all the values from the context and its parents. let mut current_values = self.all_values(); + // To ensure that there is no possible infinite loop, we need to check + // the number of iterations. If it exceeds 100, we bail. + let mut iterations: usize = 0; loop { + iterations += 1; + + if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT { + bail!("infinite loop detected in context finalization"); + } + let mut did_change = false; let mut values = BTreeMap::new(); for (key, value) in ¤t_values { @@ -176,11 +188,11 @@ impl SproutContext { } // Produce the final context. - Self { + Ok(Self { root: self.root.clone(), parent: None, values: current_values, - } + }) } /// Stamps the `text` value with the specified `values` map. The returned value indicates diff --git a/src/main.rs b/src/main.rs index f3b6474..f857ac5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,7 +143,10 @@ fn main() -> Result<()> { // Insert the values from the entry configuration into the // sprout context to use with the entry itself. context.insert(&entry.declaration().values); - let context = context.finalize().freeze(); + let context = context + .finalize() + .context("unable to finalize context")? + .freeze(); // Provide the new context to the bootable entry. entry.swap_context(context); // Restamp the title with any values. From 9f7ca672eadb5953d942dd55087e94b8331797b4 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 18:46:45 -0700 Subject: [PATCH 02/18] chore(filesystem-device-match): add clarity to statement which is unreachable --- src/extractors/filesystem_device_match.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extractors/filesystem_device_match.rs b/src/extractors/filesystem_device_match.rs index 59ebf76..54a91ec 100644 --- a/src/extractors/filesystem_device_match.rs +++ b/src/extractors/filesystem_device_match.rs @@ -81,7 +81,7 @@ pub fn extract( } else { // We should still handle other errors gracefully. Err(error).context("unable to open filesystem partition info")?; - None + unreachable!() } } } From fc710ec391002de47cd1eb8c1df289454fe719cd Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 18:47:34 -0700 Subject: [PATCH 03/18] fix(options): --help should exit with code zero --- src/options/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options/parser.rs b/src/options/parser.rs index 23b50ad..69250d8 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -131,7 +131,7 @@ pub trait OptionsRepresentable { ); } // Exit because the help has been displayed. - std::process::exit(1); + std::process::exit(0); } // Insert the option and the value into the map. From 9d2c31f77f2ce9a17f714f94bf768ea7d0373dab Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 18:49:14 -0700 Subject: [PATCH 04/18] fix(options): clarify code that checks for --abc=123 option form --- src/options/parser.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/options/parser.rs b/src/options/parser.rs index 69250d8..d6cee40 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -72,11 +72,7 @@ pub trait OptionsRepresentable { let mut value = None; // Check if the option is of the form --abc=123 - if option.contains("=") { - let Some((part_key, part_value)) = option.split_once("=") else { - bail!("invalid option: {option}"); - }; - + if let Some((part_key, part_value)) = option.split_once('=') { let part_key = part_key.to_string(); let part_value = part_value.to_string(); option = part_key; From 4c7b1d70ef8c79f13db978974bfa8b7ba6bea40b Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 18:51:34 -0700 Subject: [PATCH 05/18] fix(bls): parsing of entries should split by whitespace, not just spaces --- src/generators/bls/entry.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generators/bls/entry.rs b/src/generators/bls/entry.rs index 91c06c8..18d568d 100644 --- a/src/generators/bls/entry.rs +++ b/src/generators/bls/entry.rs @@ -36,8 +36,8 @@ impl FromStr for BlsEntry { // Trim the line. let line = line.trim(); - // Split the line once by a space. - let Some((key, value)) = line.split_once(" ") else { + // Split the line once by whitespace. + let Some((key, value)) = line.split_once(char::is_whitespace) else { continue; }; From 86fa00928ea3cd860057e407a9bb1e148a7b717f Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 18:56:11 -0700 Subject: [PATCH 06/18] fix(bls): convert less safe path concatenation to use path buffer --- src/generators/bls.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/generators/bls.rs b/src/generators/bls.rs index cd26391..b7c6b6c 100644 --- a/src/generators/bls.rs +++ b/src/generators/bls.rs @@ -6,7 +6,6 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::rc::Rc; use std::str::FromStr; -use uefi::CString16; use uefi::fs::{FileSystem, Path}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::media::fs::SimpleFileSystem; @@ -89,10 +88,9 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result Date: Fri, 24 Oct 2025 18:59:15 -0700 Subject: [PATCH 07/18] chore(perf): replace some string replacement and comparison with characters for performance --- src/generators/bls/entry.rs | 4 ++-- src/utils.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/generators/bls/entry.rs b/src/generators/bls/entry.rs index 18d568d..88790cc 100644 --- a/src/generators/bls/entry.rs +++ b/src/generators/bls/entry.rs @@ -99,7 +99,7 @@ impl BlsEntry { self.linux .clone() .or(self.efi.clone()) - .map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string()) + .map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string()) } /// Fetches the path to an initrd to pass to the kernel, if any. @@ -107,7 +107,7 @@ impl BlsEntry { pub fn initrd_path(&self) -> Option { self.initrd .clone() - .map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string()) + .map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string()) } /// Fetches the options to pass to the kernel, if any. diff --git a/src/utils.rs b/src/utils.rs index 24edafd..5d79fd6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -37,7 +37,7 @@ pub fn device_path_root(path: &DevicePath) -> Result { let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); if item .as_ref() - .map(|item| item.to_string().contains("(")) + .map(|item| item.to_string().contains('(')) .unwrap_or(false) { Some(item.unwrap_or_default()) @@ -62,7 +62,7 @@ pub fn device_path_subpath(path: &DevicePath) -> Result { let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); if item .as_ref() - .map(|item| item.to_string().contains("(")) + .map(|item| item.to_string().contains('(')) .unwrap_or(false) { None @@ -104,11 +104,11 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result Date: Fri, 24 Oct 2025 19:06:58 -0700 Subject: [PATCH 08/18] fix(splash): check for zero-sized images --- src/actions/splash.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/actions/splash.rs b/src/actions/splash.rs index a51903b..6e1ca91 100644 --- a/src/actions/splash.rs +++ b/src/actions/splash.rs @@ -52,6 +52,11 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect { height: image.height(), }; + // Handle the case where the image is zero-sized. + if input.height == 0 || input.width == 0 { + return input; + } + // Calculate the ratio of the image dimensions. let input_ratio = input.width as f32 / input.height as f32; @@ -66,6 +71,11 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect { height: frame.height, }; + // Handle the case where the output is zero-sized. + if output.height == 0 || output.width == 0 { + return output; + } + if input_ratio < frame_ratio { output.width = (frame.height as f32 * input_ratio).floor() as u32; output.height = frame.height; From 41fbca6f768b0bad18639ebb431b64386d9cd01e Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:11:17 -0700 Subject: [PATCH 09/18] fix(utils): clarify that the to_string().contains() is necessary due to CString16 --- src/utils.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 5d79fd6..1179df9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -27,6 +27,12 @@ pub fn text_to_device_path(path: &str) -> Result { .context("unable to convert text to device path") } +/// Checks if a [CString16] contains a char `c`. +/// We need to call to_string() because CString16 doesn't support `contains` with a char. +fn cstring16_contains_char(string: &CString16, c: char) -> bool { + string.to_string().contains(c) +} + /// Grabs the root part of the `path`. /// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" /// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)" @@ -37,7 +43,7 @@ pub fn device_path_root(path: &DevicePath) -> Result { let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); if item .as_ref() - .map(|item| item.to_string().contains('(')) + .map(|item| cstring16_contains_char(item, '(')) .unwrap_or(false) { Some(item.unwrap_or_default()) @@ -62,7 +68,7 @@ pub fn device_path_subpath(path: &DevicePath) -> Result { let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); if item .as_ref() - .map(|item| item.to_string().contains('(')) + .map(|item| cstring16_contains_char(item, '(')) .unwrap_or(false) { None From 7d5248e2ee56d26431467ae2134981c63677b12a Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:12:43 -0700 Subject: [PATCH 10/18] fix(context): skip over empty keys to avoid replacing $ and breaking other values --- src/context.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/context.rs b/src/context.rs index fbcbc80..71fedbb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -201,6 +201,10 @@ impl SproutContext { let mut result = text.as_ref().to_string(); let mut did_change = false; for (key, value) in values { + // Empty keys are not supported. + if key.is_empty() { + continue; + } let next_result = result.replace(&format!("${key}"), value); if result != next_result { did_change = true; From a15c92a749e49d9753d9af7e50f95e3097443d14 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:24:29 -0700 Subject: [PATCH 11/18] fix(context): ensure longer keys are replaced first, fixing key replacement edge case --- src/context.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/context.rs b/src/context.rs index 71fedbb..a804f83 100644 --- a/src/context.rs +++ b/src/context.rs @@ -200,11 +200,23 @@ impl SproutContext { fn stamp_values(values: &BTreeMap, text: impl AsRef) -> (bool, String) { let mut result = text.as_ref().to_string(); let mut did_change = false; - for (key, value) in values { + + // Sort the keys by length. This is to ensure that we stamp the longest keys first. + // If we did not do this, "$abc" could be stamped by "$a" into an invalid result. + let mut keys = values.keys().collect::>(); + keys.sort_by_key(|key| key.len()); + + for key in keys { // Empty keys are not supported. if key.is_empty() { continue; } + + // We can fetch the value from the map. It is verifiable that the key exists. + let Some(value) = values.get(key) else { + unreachable!("keys iterated over is collected on a map that cannot be modified"); + }; + let next_result = result.replace(&format!("${key}"), value); if result != next_result { did_change = true; From 482db0b763f18b4aa8d227ff6acc862e8a597a77 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:27:43 -0700 Subject: [PATCH 12/18] fix(media-loader): eliminate usage of unwrap and swap to result --- src/utils/media_loader.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/media_loader.rs b/src/utils/media_loader.rs index d8840ac..f3f4954 100644 --- a/src/utils/media_loader.rs +++ b/src/utils/media_loader.rs @@ -97,7 +97,7 @@ impl MediaLoaderHandle { } /// Creates a new device path for the media loader based on a vendor `guid`. - fn device_path(guid: Guid) -> Box { + fn device_path(guid: Guid) -> Result> { // The buffer for the device path. let mut path = Vec::new(); // Build a device path for the media loader with a vendor-specific guid. @@ -106,18 +106,18 @@ impl MediaLoaderHandle { vendor_guid: guid, vendor_defined_data: &[], }) - .unwrap() // We know that the device path is valid, so we can unwrap. + .context("unable to produce device path")? .finalize() - .unwrap(); // We know that the device path is valid, so we can unwrap. + .context("unable to produce device path")?; // Convert the device path to a boxed device path. // This is safer than dealing with a pooled device path. - path.to_boxed() + Ok(path.to_boxed()) } /// Checks if the media loader is already registered with the UEFI stack. fn already_registered(guid: Guid) -> Result { // Acquire the device path for the media loader. - let path = Self::device_path(guid); + let path = Self::device_path(guid)?; let mut existing_path = path.as_ref(); @@ -142,7 +142,7 @@ impl MediaLoaderHandle { /// to load the data from. pub fn register(guid: Guid, data: Box<[u8]>) -> Result { // Acquire the vendor device path for the media loader. - let path = Self::device_path(guid); + let path = Self::device_path(guid)?; // Check if the media loader is already registered. // If it is, we can't register it again safely. From 45d7cd2d3b0c2e66af8d97ce001bee183d9c893f Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:28:38 -0700 Subject: [PATCH 13/18] fix(doc): incorrect comment for startup phase execution --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f857ac5..9a5d5bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,7 +107,7 @@ fn main() -> Result<()> { context.insert(&extracted); let context = context.freeze(); - // Execute the late phase. + // Execute the startup phase. phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?; let mut entries = Vec::new(); From 057c48f9f788e7b5cbe789cad34fa5979058b8d3 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:31:01 -0700 Subject: [PATCH 14/18] fix(bls): parser should skip over empty lines and comments --- src/generators/bls/entry.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/generators/bls/entry.rs b/src/generators/bls/entry.rs index 88790cc..f9c85c0 100644 --- a/src/generators/bls/entry.rs +++ b/src/generators/bls/entry.rs @@ -36,6 +36,11 @@ impl FromStr for BlsEntry { // Trim the line. let line = line.trim(); + // Skip over empty lines and comments. + if line.is_empty() || line.starts_with('#') { + continue; + } + // Split the line once by whitespace. let Some((key, value)) = line.split_once(char::is_whitespace) else { continue; From 2253fa2a1f6b4cc3db3b67d1bfbf3d47e925a945 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:37:06 -0700 Subject: [PATCH 15/18] fix(context): make sure to actually iterate longest first for key replacement --- src/context.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/context.rs b/src/context.rs index a804f83..e299325 100644 --- a/src/context.rs +++ b/src/context.rs @@ -2,6 +2,7 @@ use crate::actions::ActionDeclaration; use crate::options::SproutOptions; use anyhow::anyhow; use anyhow::{Result, bail}; +use std::cmp::Reverse; use std::collections::{BTreeMap, BTreeSet}; use std::rc::Rc; use uefi::proto::device_path::DevicePath; @@ -204,7 +205,9 @@ impl SproutContext { // Sort the keys by length. This is to ensure that we stamp the longest keys first. // If we did not do this, "$abc" could be stamped by "$a" into an invalid result. let mut keys = values.keys().collect::>(); - keys.sort_by_key(|key| key.len()); + + // Sort by key length, reversed. This results in the longest keys appearing first. + keys.sort_by_key(|key| Reverse(key.len())); for key in keys { // Empty keys are not supported. From e243228f151abf9eaf1980182c8d9ed8f6a20dab Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:44:26 -0700 Subject: [PATCH 16/18] fix(framebuffer): check width, height and implement proper checking when accessing pixels --- src/actions/splash.rs | 3 ++- src/utils/framebuffer.rs | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/actions/splash.rs b/src/actions/splash.rs index 6e1ca91..991aecf 100644 --- a/src/actions/splash.rs +++ b/src/actions/splash.rs @@ -120,7 +120,8 @@ fn draw(image: DynamicImage) -> Result<()> { let image = resize_to_fit(&image, fit); // Create a framebuffer to draw the image on. - let mut framebuffer = Framebuffer::new(width, height); + let mut framebuffer = + Framebuffer::new(width, height).context("unable to create framebuffer")?; // Iterate over the pixels in the image and put them on the framebuffer. for (x, y, pixel) in image.enumerate_pixels() { diff --git a/src/utils/framebuffer.rs b/src/utils/framebuffer.rs index 7fcf55a..6b3afc4 100644 --- a/src/utils/framebuffer.rs +++ b/src/utils/framebuffer.rs @@ -13,17 +13,28 @@ pub struct Framebuffer { impl Framebuffer { /// Creates a new framebuffer of the specified `width` and `height`. - pub fn new(width: usize, height: usize) -> Self { - Framebuffer { + pub fn new(width: usize, height: usize) -> Result { + // Verify that the size is valid during multiplication. + let size = width + .checked_mul(height) + .context("framebuffer size overflow")?; + + // Initialize the pixel buffer with black pixels, with the verified size. + let pixels = vec![BltPixel::new(0, 0, 0); size]; + + Ok(Framebuffer { width, height, - pixels: vec![BltPixel::new(0, 0, 0); width * height], - } + pixels, + }) } /// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate. pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> { - self.pixels.get_mut(y * self.width + x) + // Calculate the index of the pixel safely, returning None if it overflows. + let index = y.checked_mul(self.width)?.checked_add(x)?; + // Return the pixel at the index. If the index is out of bounds, this will return None. + self.pixels.get_mut(index) } /// Blit the framebuffer to the specified `gop` [GraphicsOutput]. From 6cd502ef1819be4902c6735444bf40718ab5315a Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:51:08 -0700 Subject: [PATCH 17/18] fix(actions): if edera action returns successfully, an intended unreachable line could be reached --- src/actions.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/actions.rs b/src/actions.rs index 2893d9f..8bd1031 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -64,6 +64,7 @@ pub fn execute(context: Rc, name: impl AsRef) -> Result<()> return Ok(()); } else if let Some(edera) = &action.edera { edera::edera(context.clone(), edera)?; + return Ok(()); } #[cfg(feature = "splash")] From 0c2303d789ef0c39051ea61e4f64ff7a082d18b9 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 24 Oct 2025 19:54:28 -0700 Subject: [PATCH 18/18] fix(framebuffer): add proper bounds checking for accessing a pixel --- src/utils/framebuffer.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/framebuffer.rs b/src/utils/framebuffer.rs index 6b3afc4..084df36 100644 --- a/src/utils/framebuffer.rs +++ b/src/utils/framebuffer.rs @@ -31,6 +31,11 @@ impl Framebuffer { /// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate. pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> { + // Verify that the coordinates are within the bounds of the framebuffer. + if x >= self.width || y >= self.height { + return None; + } + // Calculate the index of the pixel safely, returning None if it overflows. let index = y.checked_mul(self.width)?.checked_add(x)?; // Return the pixel at the index. If the index is out of bounds, this will return None.