fix(cr-2026-04-22): recording_filename avoids same-second collisions
MAJOR from the 2026-04-22 review (audio.rs:236-257): filenames were derived from SystemTime::now().as_secs(), so two recordings started within the same second resolved to the same path — possible overwrite or merge. Extracts the filename generation into a private helper and appends the sub-second nanosecond component. Format is now `kon-<secs>-<nanos_in_sec>.wav`, e.g. `kon-1776828000-123456789.wav`, which stays human-readable, sortable by timestamp, and effectively uncollidable under any realistic live-capture pattern. Two tests cover the regression: - recording_filenames_are_unique_across_rapid_calls: 64 tight-loop calls must produce at least 32 unique names. - recording_filename_has_expected_shape: prefix/suffix plus the zero-padded 9-digit nanos component.
This commit is contained in:
@@ -249,11 +249,62 @@ pub fn resolve_recording_path(
|
|||||||
std::fs::create_dir_all(&recordings_dir)
|
std::fs::create_dir_all(&recordings_dir)
|
||||||
.map_err(|e| format!("Failed to create recordings dir: {e}"))?;
|
.map_err(|e| format!("Failed to create recordings dir: {e}"))?;
|
||||||
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
Ok(recordings_dir.join(recording_filename()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic recording filename generator. Uses seconds since the
|
||||||
|
/// epoch for human-readable sortability plus the sub-second nanosecond
|
||||||
|
/// component so two recordings started within the same second do not
|
||||||
|
/// collide (brief item from the 2026-04-22 code review).
|
||||||
|
///
|
||||||
|
/// Format: `kon-<secs>-<nanos_in_sec>.wav`, e.g.
|
||||||
|
/// `kon-1776828000-123456789.wav`.
|
||||||
|
fn recording_filename() -> String {
|
||||||
|
let duration = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
.as_secs();
|
let secs = duration.as_secs();
|
||||||
Ok(recordings_dir.join(format!("kon-{timestamp}.wav")))
|
let nanos = duration.subsec_nanos();
|
||||||
|
format!("kon-{secs}-{nanos:09}.wav")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::recording_filename;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recording_filenames_are_unique_across_rapid_calls() {
|
||||||
|
// Regression for the 2026-04-22 review: two recordings
|
||||||
|
// started in the same second previously collided on a
|
||||||
|
// seconds-only filename. The sub-second precision added
|
||||||
|
// here must hold under a tight loop.
|
||||||
|
let mut names = std::collections::HashSet::new();
|
||||||
|
for _ in 0..64 {
|
||||||
|
names.insert(recording_filename());
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
names.len() >= 32,
|
||||||
|
"expected at least 32 unique filenames across 64 rapid calls, got {}",
|
||||||
|
names.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recording_filename_has_expected_shape() {
|
||||||
|
let name = recording_filename();
|
||||||
|
assert!(name.starts_with("kon-"));
|
||||||
|
assert!(name.ends_with(".wav"));
|
||||||
|
// Shape: kon-<digits>-<9 digits>.wav
|
||||||
|
let rest = name
|
||||||
|
.strip_prefix("kon-")
|
||||||
|
.and_then(|s| s.strip_suffix(".wav"))
|
||||||
|
.expect("shape prefix/suffix");
|
||||||
|
let parts: Vec<&str> = rest.split('-').collect();
|
||||||
|
assert_eq!(parts.len(), 2, "expected two '-' separated parts, got {parts:?}");
|
||||||
|
assert!(parts[0].chars().all(|c| c.is_ascii_digit()), "secs is digits");
|
||||||
|
assert_eq!(parts[1].len(), 9, "nanos component is zero-padded to 9 digits");
|
||||||
|
assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn persist_audio_samples(
|
pub async fn persist_audio_samples(
|
||||||
|
|||||||
Reference in New Issue
Block a user