chore(hardening): tighten security and footprint defaults
This commit is contained in:
@@ -1,19 +1,24 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tauri::{Emitter, Manager};
|
||||
use tokio::sync::{mpsc as tokio_mpsc, Mutex as AsyncMutex};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use kon_audio::{DeviceInfo, MicrophoneCapture};
|
||||
use crate::commands::security::ensure_main_window;
|
||||
use kon_audio::{DeviceInfo, MicrophoneCapture, WavWriter};
|
||||
use kon_core::constants::WHISPER_SAMPLE_RATE;
|
||||
use kon_core::types::AudioSamples;
|
||||
|
||||
const MAX_NATIVE_CAPTURE_RETURN_SAMPLES: usize = WHISPER_SAMPLE_RATE as usize * 60 * 10;
|
||||
|
||||
/// Enumerate every input device available to cpal, with metadata for the
|
||||
/// Settings device-picker UI. Includes a flag for likely PulseAudio /
|
||||
/// PipeWire monitor sources so the UI can warn the user.
|
||||
#[tauri::command]
|
||||
pub async fn list_audio_devices() -> Result<Vec<DeviceInfo>, String> {
|
||||
pub async fn list_audio_devices(window: tauri::WebviewWindow) -> Result<Vec<DeviceInfo>, String> {
|
||||
ensure_main_window(&window)?;
|
||||
tokio::task::spawn_blocking(MicrophoneCapture::list_devices)
|
||||
.await
|
||||
.map_err(|e| format!("join error: {e}"))?
|
||||
@@ -51,8 +56,12 @@ pub struct NativeCaptureState {
|
||||
/// holding the lock — a `std::sync::Mutex` would have to be released
|
||||
/// and reacquired around each await point.
|
||||
worker: AsyncMutex<Option<CaptureWorker>>,
|
||||
/// All captured samples (16kHz mono) for save_audio.
|
||||
/// Compatibility buffer returned by stop_native_capture. Capped; the
|
||||
/// authoritative capture is streamed to temp WAV on disk.
|
||||
all_samples: Arc<Mutex<Vec<f32>>>,
|
||||
wav_writer: Arc<Mutex<Option<WavWriter>>>,
|
||||
temp_audio_path: Arc<Mutex<Option<PathBuf>>>,
|
||||
capture_truncated: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl NativeCaptureState {
|
||||
@@ -60,6 +69,36 @@ impl NativeCaptureState {
|
||||
Self {
|
||||
worker: AsyncMutex::new(None),
|
||||
all_samples: Arc::new(Mutex::new(Vec::new())),
|
||||
wav_writer: Arc::new(Mutex::new(None)),
|
||||
temp_audio_path: Arc::new(Mutex::new(None)),
|
||||
capture_truncated: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_recorded_chunk(
|
||||
all_samples: &Arc<Mutex<Vec<f32>>>,
|
||||
wav_writer: &Arc<Mutex<Option<WavWriter>>>,
|
||||
truncated: &Arc<AtomicBool>,
|
||||
chunk: &[f32],
|
||||
) {
|
||||
if let Ok(mut writer) = wav_writer.lock() {
|
||||
if let Some(writer) = writer.as_mut() {
|
||||
if let Err(e) = writer.append(chunk) {
|
||||
eprintln!("[native-capture] temp WAV append failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut all) = all_samples.lock() {
|
||||
let remaining = MAX_NATIVE_CAPTURE_RETURN_SAMPLES.saturating_sub(all.len());
|
||||
if remaining >= chunk.len() {
|
||||
all.extend_from_slice(chunk);
|
||||
} else {
|
||||
if remaining > 0 {
|
||||
all.extend_from_slice(&chunk[..remaining]);
|
||||
}
|
||||
truncated.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,10 +111,12 @@ impl NativeCaptureState {
|
||||
/// user's pick from Settings → Audio → Microphone takes effect.
|
||||
#[tauri::command]
|
||||
pub async fn start_native_capture(
|
||||
window: tauri::WebviewWindow,
|
||||
app: tauri::AppHandle,
|
||||
state: tauri::State<'_, NativeCaptureState>,
|
||||
device_name: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
ensure_main_window(&window)?;
|
||||
eprintln!(
|
||||
"[native-capture] start_native_capture called (device='{}')",
|
||||
device_name.as_deref().unwrap_or("<auto>")
|
||||
@@ -116,10 +157,19 @@ pub async fn start_native_capture(
|
||||
|
||||
let all_samples = state.all_samples.clone();
|
||||
all_samples.lock().unwrap().clear();
|
||||
state.capture_truncated.store(false, Ordering::Relaxed);
|
||||
|
||||
let temp_path = std::env::temp_dir().join(recording_filename());
|
||||
let writer = WavWriter::create(&temp_path, WHISPER_SAMPLE_RATE, 1)
|
||||
.map_err(|e| format!("Failed to create temp capture WAV: {e}"))?;
|
||||
*state.wav_writer.lock().unwrap() = Some(writer);
|
||||
*state.temp_audio_path.lock().unwrap() = Some(temp_path);
|
||||
|
||||
let (stop_tx, mut stop_rx) = tokio_mpsc::channel::<()>(1);
|
||||
|
||||
let all_samples_clone = all_samples.clone();
|
||||
let wav_writer_clone = state.wav_writer.clone();
|
||||
let truncated_clone = state.capture_truncated.clone();
|
||||
|
||||
// Spawn a task that reads cpal chunks, downsamples to 16kHz mono,
|
||||
// and emits events to the frontend. The JoinHandle is retained in
|
||||
@@ -189,10 +239,12 @@ pub async fn start_native_capture(
|
||||
while pcm_buffer.len() >= chunk_size {
|
||||
let chunk: Vec<f32> = pcm_buffer.drain(..chunk_size).collect();
|
||||
|
||||
// Store for save_audio
|
||||
if let Ok(mut all) = all_samples_clone.lock() {
|
||||
all.extend_from_slice(&chunk);
|
||||
}
|
||||
append_recorded_chunk(
|
||||
&all_samples_clone,
|
||||
&wav_writer_clone,
|
||||
&truncated_clone,
|
||||
&chunk,
|
||||
);
|
||||
|
||||
let _ = app.emit(
|
||||
"native-pcm",
|
||||
@@ -213,9 +265,12 @@ pub async fn start_native_capture(
|
||||
|
||||
// Emit any remaining samples
|
||||
if !pcm_buffer.is_empty() {
|
||||
if let Ok(mut all) = all_samples_clone.lock() {
|
||||
all.extend_from_slice(&pcm_buffer);
|
||||
}
|
||||
append_recorded_chunk(
|
||||
&all_samples_clone,
|
||||
&wav_writer_clone,
|
||||
&truncated_clone,
|
||||
&pcm_buffer,
|
||||
);
|
||||
let _ = app.emit(
|
||||
"native-pcm",
|
||||
serde_json::json!({
|
||||
@@ -228,6 +283,13 @@ pub async fn start_native_capture(
|
||||
if let Ok(mut cap) = capture_clone.lock() {
|
||||
cap.take();
|
||||
}
|
||||
if let Ok(mut writer) = wav_writer_clone.lock() {
|
||||
if let Some(writer) = writer.take() {
|
||||
if let Err(e) = writer.finalize() {
|
||||
eprintln!("[native-capture] temp WAV finalize failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*state.worker.lock().await = Some(CaptureWorker { stop_tx, join });
|
||||
@@ -242,8 +304,10 @@ pub async fn start_native_capture(
|
||||
/// nothing from a worker that technically outlived the call (RB-06).
|
||||
#[tauri::command]
|
||||
pub async fn stop_native_capture(
|
||||
window: tauri::WebviewWindow,
|
||||
state: tauri::State<'_, NativeCaptureState>,
|
||||
) -> Result<Vec<f32>, String> {
|
||||
ensure_main_window(&window)?;
|
||||
if let Some(worker) = state.worker.lock().await.take() {
|
||||
stop_worker(worker).await;
|
||||
}
|
||||
@@ -252,6 +316,11 @@ pub async fn stop_native_capture(
|
||||
let mut all = state.all_samples.lock().unwrap();
|
||||
std::mem::take(&mut *all)
|
||||
};
|
||||
if state.capture_truncated.load(Ordering::Relaxed) {
|
||||
eprintln!(
|
||||
"[native-capture] stop_native_capture returned a capped compatibility buffer; temp WAV contains the full capture"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(samples)
|
||||
}
|
||||
@@ -461,9 +530,11 @@ pub async fn persist_audio_samples(
|
||||
/// Save PCM f32 samples as a WAV file. Returns the file path.
|
||||
#[tauri::command]
|
||||
pub async fn save_audio(
|
||||
window: tauri::WebviewWindow,
|
||||
app: tauri::AppHandle,
|
||||
samples: Vec<f32>,
|
||||
output_folder: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
ensure_main_window(&window)?;
|
||||
persist_audio_samples(&app, samples, output_folder).await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user