Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ chrono = { version = "0.4.45", default-features = false, features = ["std"] }
# Typed errors inside the db/excel connector (stringified only at the IPC edge).
# Already transitive via sqlx.
thiserror = "1"
# Canonicalize without the \\?\ prefix (scope.rs): stored grants must match
# user-visible paths or revoke's literal fallback can't find them.
dunce = "1"

[target.'cfg(unix)'.dependencies]
# Linux: O_NONBLOCK for the usblp read-back fd (AsyncFd needs a non-blocking
Expand Down
22 changes: 20 additions & 2 deletions src-tauri/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ enum DbError {
PasswordMalformed,
#[error("sqlite has no password")]
SqliteNoPassword,
#[error("file access not granted; re-select the database file in the profile")]
PathNotAllowed,
#[error(transparent)]
Join(#[from] tauri::Error),
}
Expand Down Expand Up @@ -254,6 +256,8 @@ async fn keychain_password(profile_id: &str, endpoint: &str) -> Result<Option<St
password_for_endpoint(blob.as_deref(), endpoint)
}

/// Callers reached from a `#[tauri::command]` must run `check_path_scope`
/// first: a sqlite path is only trustworthy after the path-grant gate.
async fn connect(spec: &DbSpec) -> Result<DbConn, DbError> {
// Resolved BEFORE the network timeout: a keychain read can block on an OS
// permission prompt, which must not eat the connect budget.
Expand Down Expand Up @@ -507,8 +511,21 @@ async fn run_list_tables(spec: &DbSpec) -> Result<Vec<String>, DbError> {
out
}

/// Refuse a sqlite path the user never granted via the native pick command
/// (scope::PathGrants); network specs carry no path.
fn check_path_scope(app: &tauri::AppHandle, spec: &DbSpec) -> Result<(), DbError> {
use tauri::Manager;
match spec {
DbSpec::Sqlite { path } if !app.state::<crate::scope::PathGrants>().is_granted(path) => {
Err(DbError::PathNotAllowed)
}
_ => Ok(()),
}
}

#[tauri::command]
pub async fn db_list_tables(spec: DbSpec) -> Result<Vec<String>, String> {
pub async fn db_list_tables(app: tauri::AppHandle, spec: DbSpec) -> Result<Vec<String>, String> {
check_path_scope(&app, &spec).map_err(|e| e.to_string())?;
run_list_tables(&spec).await.map_err(|e| e.to_string())
}

Expand All @@ -525,7 +542,8 @@ async fn run_fetch(spec: &DbSpec, table: &str) -> Result<Rows, DbError> {
}

#[tauri::command]
pub async fn db_fetch(spec: DbSpec, table: String) -> Result<Rows, String> {
pub async fn db_fetch(app: tauri::AppHandle, spec: DbSpec, table: String) -> Result<Rows, String> {
check_path_scope(&app, &spec).map_err(|e| e.to_string())?;
run_fetch(&spec, &table).await.map_err(|e| e.to_string())
}

Expand Down
22 changes: 20 additions & 2 deletions src-tauri/src/excel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ enum ExcelError {
TooLargeUncompressed,
#[error("empty sheet: {0}")]
EmptySheet(String),
#[error("file access not granted; re-select the file")]
PathNotAllowed,
#[error(transparent)]
Join(#[from] tauri::Error),
}
Expand Down Expand Up @@ -77,11 +79,22 @@ async fn timed_read<T>(
parsed?
}

/// Refuse a path the user never granted via the native pick command
/// (scope::PathGrants).
fn check_path_scope(app: &tauri::AppHandle, path: &str) -> Result<(), ExcelError> {
use tauri::Manager;
if !app.state::<crate::scope::PathGrants>().is_granted(path) {
return Err(ExcelError::PathNotAllowed);
}
Ok(())
}

// calamine parses under the release panic="abort" profile, so a panic on a
// crafted/corrupt workbook aborts the app instead of returning Err. Accepted;
// check_size + the dialog-only pick keep realistic inputs benign.
#[tauri::command]
pub async fn excel_list_sheets(path: String) -> Result<Vec<String>, String> {
pub async fn excel_list_sheets(app: tauri::AppHandle, path: String) -> Result<Vec<String>, String> {
check_path_scope(&app, &path).map_err(|e| e.to_string())?;
timed_read(tauri::async_runtime::spawn_blocking(
move || -> Result<Vec<String>, ExcelError> {
check_size(&path)?;
Expand All @@ -94,7 +107,12 @@ pub async fn excel_list_sheets(path: String) -> Result<Vec<String>, String> {
}

#[tauri::command]
pub async fn excel_fetch(path: String, sheet: String) -> Result<Rows, String> {
pub async fn excel_fetch(
app: tauri::AppHandle,
path: String,
sheet: String,
) -> Result<Rows, String> {
check_path_scope(&app, &path).map_err(|e| e.to_string())?;
timed_read(tauri::async_runtime::spawn_blocking(
move || -> Result<Rows, ExcelError> {
check_size(&path)?;
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod db;
mod excel;
mod mcp;
mod print;
mod scope;
mod transport;
mod usb;

Expand All @@ -13,6 +14,12 @@ use tauri::Manager;
pub fn run() {
let builder = tauri::Builder::default()
.manage(mcp::McpState::default())
// In setup, not manage(): loading the persisted grants needs the app paths.
.setup(|app| {
let grants = scope::PathGrants::load(app.handle());
app.manage(grants);
Ok(())
})
.invoke_handler(tauri::generate_handler![
print::send_zpl_tcp,
print::query_zpl_tcp,
Expand All @@ -27,6 +34,9 @@ pub fn run() {
db::db_set_password,
excel::excel_list_sheets,
excel::excel_fetch,
scope::pick_sqlite_file,
scope::pick_excel_file,
scope::revoke_db_path,
credentials::credential_get,
credentials::credential_set,
credentials::credential_delete,
Expand Down
Loading