From 5f50b776764bae05f7fca58896a4355b19f5b3fe Mon Sep 17 00:00:00 2001
From: Peter Li
Date: Sun, 8 Feb 2026 00:48:05 -0800
Subject: [PATCH] integer overflow fixes
---
src/db.rs | 37 ++++++++++++++++++-
src/handlers.rs | 80 ++++++++++++++++++++++++++++++++++++++++
src/main.rs | 4 ++
templates/day.html | 2 +
templates/planning.html | 1 +
templates/subscribe.html | 25 +++++++++----
6 files changed, 140 insertions(+), 9 deletions(-)
diff --git a/src/db.rs b/src/db.rs
index 65d3cb8..76d5080 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -2,6 +2,8 @@ use std::collections::HashMap;
use rusqlite::{Connection, OptionalExtension, params};
+pub const MAX_CALORIES_VALUE: i64 = 1_000_000;
+
#[derive(Clone)]
pub struct FoodEntry {
pub id: i64,
@@ -82,7 +84,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
user_id INTEGER NOT NULL,
entry_date TEXT NOT NULL,
name TEXT NOT NULL,
- calories INTEGER NOT NULL CHECK (calories >= 0),
+ calories INTEGER NOT NULL CHECK (calories >= 0 AND calories <= 1000000),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_food_entries_user_date
@@ -100,7 +102,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
CREATE TABLE IF NOT EXISTS user_planning (
user_id INTEGER PRIMARY KEY,
target_weight REAL,
- target_calories INTEGER CHECK (target_calories >= 0),
+ target_calories INTEGER CHECK (target_calories >= 0 AND target_calories <= 1000000),
bmr REAL CHECK (bmr > 0),
public_entries INTEGER NOT NULL DEFAULT 0,
public_weights INTEGER NOT NULL DEFAULT 0,
@@ -137,6 +139,8 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
[],
)?;
}
+
+ sanitize_calorie_values(conn, MAX_CALORIES_VALUE)?;
Ok(())
}
@@ -245,6 +249,19 @@ pub fn create_subscription(
Ok(changed > 0)
}
+pub fn delete_subscription(
+ conn: &Connection,
+ subscriber_user_id: i64,
+ target_user_id: i64,
+) -> Result {
+ let changed = conn.execute(
+ "DELETE FROM subscriptions
+ WHERE subscriber_user_id = ?1 AND target_user_id = ?2",
+ params![subscriber_user_id, target_user_id],
+ )?;
+ Ok(changed > 0)
+}
+
pub fn fetch_subscription_targets(
conn: &Connection,
subscriber_user_id: i64,
@@ -695,3 +712,19 @@ fn table_has_column(
}
Ok(false)
}
+
+fn sanitize_calorie_values(conn: &Connection, max_calories: i64) -> Result<(), rusqlite::Error> {
+ conn.execute(
+ "UPDATE user_food_entries
+ SET calories = ?1
+ WHERE calories > ?1",
+ params![max_calories],
+ )?;
+ conn.execute(
+ "UPDATE user_planning
+ SET target_calories = ?1
+ WHERE target_calories > ?1",
+ params![max_calories],
+ )?;
+ Ok(())
+}
diff --git a/src/handlers.rs b/src/handlers.rs
index 3f8a7ca..58ba81f 100644
--- a/src/handlers.rs
+++ b/src/handlers.rs
@@ -288,6 +288,65 @@ pub async fn subscribe_to_user(
}
}
+pub async fn unsubscribe_from_user(
+ State(state): State,
+ headers: HeaderMap,
+ Form(form): Form>,
+) -> Result {
+ let auth = get_auth_user(&state, &headers).await?;
+ let Some(auth) = auth else {
+ return Ok(Redirect::to("/login").into_response());
+ };
+
+ let username = form.get("username").map(|v| v.trim()).unwrap_or("");
+ if username.is_empty() {
+ return render_subscribe_page_with_messages(
+ &state,
+ &auth,
+ "",
+ "Username is required",
+ )
+ .await;
+ }
+
+ let target = {
+ let db_conn = state.db.lock().await;
+ db::fetch_user_by_username(&db_conn, username).map_err(internal_db_error)?
+ };
+ let Some(target) = target else {
+ return render_subscribe_page_with_messages(
+ &state,
+ &auth,
+ "",
+ "User not found",
+ )
+ .await;
+ };
+
+ let removed = {
+ let db_conn = state.db.lock().await;
+ db::delete_subscription(&db_conn, auth.user.id, target.id).map_err(internal_db_error)?
+ };
+
+ if removed {
+ render_subscribe_page_with_messages(
+ &state,
+ &auth,
+ &format!("Unsubscribed from @{}", target.username),
+ "",
+ )
+ .await
+ } else {
+ render_subscribe_page_with_messages(
+ &state,
+ &auth,
+ "",
+ &format!("You were not subscribed to @{}", target.username),
+ )
+ .await
+ }
+}
+
pub async fn show_inbox(
State(state): State,
headers: HeaderMap,
@@ -1419,6 +1478,12 @@ fn parse_entry_form_fields(form: &HashMap) -> Result<(String, i6
if calories < 0 {
return Err((StatusCode::BAD_REQUEST, "Calories must be >= 0".to_string()));
}
+ if calories > db::MAX_CALORIES_VALUE {
+ return Err((
+ StatusCode::BAD_REQUEST,
+ format!("Calories must be <= {}", db::MAX_CALORIES_VALUE),
+ ));
+ }
Ok((name.to_string(), calories))
}
@@ -1449,6 +1514,15 @@ fn parse_new_entry_form_fields(form: &HashMap) -> Result<(String
StatusCode::BAD_REQUEST,
"Calories total is too large".to_string(),
))?;
+ if total > db::MAX_CALORIES_VALUE {
+ return Err((
+ StatusCode::BAD_REQUEST,
+ format!(
+ "Calories total must be <= {}",
+ db::MAX_CALORIES_VALUE
+ ),
+ ));
+ }
Ok((name, total))
}
@@ -1494,6 +1568,12 @@ fn parse_optional_non_negative_i64(raw: Option<&String>) -> Result
{% else %}
-