integer overflow fixes

This commit is contained in:
Peter Li 2026-02-08 00:48:05 -08:00
parent 97412a8589
commit 5f50b77676
6 changed files with 140 additions and 9 deletions

View File

@ -2,6 +2,8 @@ use std::collections::HashMap;
use rusqlite::{Connection, OptionalExtension, params}; use rusqlite::{Connection, OptionalExtension, params};
pub const MAX_CALORIES_VALUE: i64 = 1_000_000;
#[derive(Clone)] #[derive(Clone)]
pub struct FoodEntry { pub struct FoodEntry {
pub id: i64, pub id: i64,
@ -82,7 +84,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
entry_date TEXT NOT NULL, entry_date TEXT NOT NULL,
name 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 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX IF NOT EXISTS idx_user_food_entries_user_date 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 ( CREATE TABLE IF NOT EXISTS user_planning (
user_id INTEGER PRIMARY KEY, user_id INTEGER PRIMARY KEY,
target_weight REAL, 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), bmr REAL CHECK (bmr > 0),
public_entries INTEGER NOT NULL DEFAULT 0, public_entries INTEGER NOT NULL DEFAULT 0,
public_weights 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(()) Ok(())
} }
@ -245,6 +249,19 @@ pub fn create_subscription(
Ok(changed > 0) Ok(changed > 0)
} }
pub fn delete_subscription(
conn: &Connection,
subscriber_user_id: i64,
target_user_id: i64,
) -> Result<bool, rusqlite::Error> {
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( pub fn fetch_subscription_targets(
conn: &Connection, conn: &Connection,
subscriber_user_id: i64, subscriber_user_id: i64,
@ -695,3 +712,19 @@ fn table_has_column(
} }
Ok(false) 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(())
}

View File

@ -288,6 +288,65 @@ pub async fn subscribe_to_user(
} }
} }
pub async fn unsubscribe_from_user(
State(state): State<AppState>,
headers: HeaderMap,
Form(form): Form<HashMap<String, String>>,
) -> Result<Response, AppError> {
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( pub async fn show_inbox(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
@ -1419,6 +1478,12 @@ fn parse_entry_form_fields(form: &HashMap<String, String>) -> Result<(String, i6
if calories < 0 { if calories < 0 {
return Err((StatusCode::BAD_REQUEST, "Calories must be >= 0".to_string())); 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)) Ok((name.to_string(), calories))
} }
@ -1449,6 +1514,15 @@ fn parse_new_entry_form_fields(form: &HashMap<String, String>) -> Result<(String
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"Calories total is too large".to_string(), "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)) Ok((name, total))
} }
@ -1494,6 +1568,12 @@ fn parse_optional_non_negative_i64(raw: Option<&String>) -> Result<Option<i64>,
"Target calories must be >= 0".to_string(), "Target calories must be >= 0".to_string(),
)); ));
} }
if parsed > db::MAX_CALORIES_VALUE {
return Err((
StatusCode::BAD_REQUEST,
format!("Target calories must be <= {}", db::MAX_CALORIES_VALUE),
));
}
Ok(Some(parsed)) Ok(Some(parsed))
} }
} }

View File

@ -48,6 +48,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
) )
.route("/subscribe", get(handlers::show_subscribe)) .route("/subscribe", get(handlers::show_subscribe))
.route("/subscribe", post(handlers::subscribe_to_user)) .route("/subscribe", post(handlers::subscribe_to_user))
.route(
"/subscribe/unsubscribe",
post(handlers::unsubscribe_from_user),
)
.route("/u/{username}", get(handlers::show_public_profile)) .route("/u/{username}", get(handlers::show_public_profile))
.route("/u/{username}/day/{date}", get(handlers::show_public_day)) .route("/u/{username}/day/{date}", get(handlers::show_public_day))
.route("/reports", get(handlers::show_reports)) .route("/reports", get(handlers::show_reports))

View File

@ -46,6 +46,7 @@
name="calories" name="calories"
placeholder="Calories each" placeholder="Calories each"
min="0" min="0"
max="1000000"
required required
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none" class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
/> />
@ -90,6 +91,7 @@
name="calories" name="calories"
value="{{ entry.calories }}" value="{{ entry.calories }}"
min="0" min="0"
max="1000000"
required required
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none" class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
/> />

View File

@ -26,6 +26,7 @@
<input <input
type="number" type="number"
min="0" min="0"
max="1000000"
name="target_calories" name="target_calories"
value="{{ page.target_calories_value }}" value="{{ page.target_calories_value }}"
placeholder="e.g. 2200" placeholder="e.g. 2200"

View File

@ -30,14 +30,25 @@
{% if page.subscriptions.is_empty() %} {% if page.subscriptions.is_empty() %}
<p class="mt-2 text-sm text-slate-600">No subscriptions yet.</p> <p class="mt-2 text-sm text-slate-600">No subscriptions yet.</p>
{% else %} {% else %}
<div class="mt-3 flex flex-wrap gap-2"> <div class="mt-3 grid gap-2">
{% for username in page.subscriptions %} {% for username in page.subscriptions %}
<div class="flex flex-wrap items-center gap-2">
<a <a
href="/u/{{ username }}" href="/u/{{ username }}"
class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100"
> >
@{{ username }} @{{ username }}
</a> </a>
<form method="post" action="/subscribe/unsubscribe">
<input type="hidden" name="username" value="{{ username }}" />
<button
type="submit"
class="rounded-lg border border-rose-300 bg-rose-50 px-3 py-2 text-sm font-semibold text-rose-700 hover:bg-rose-100"
>
Unsubscribe
</button>
</form>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}