diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..92f7d92 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,49 @@ +CHangelog + +## Dev + +decoder.rs +# What Why +1 Removed Clone bound; pass on_progress by &impl Fn(f32) Eliminates closure cloning on every phase-offset iteration +2 SYNC_BITS → compile-time const [bool; 16] Eliminates Vec heap allocation on every call to find_frame_in_bits +3 is_multiple_of(32) → % 32 == 0 is_multiple_of only stabilized in Rust 1.85; modulo is universally supported +4 Pass precomputed spb into samples_to_bits Avoids redundant f64::from / f64::from division +5 Vec::with_capacity(estimated) Pre-allocates the bit vector; avoids repeated reallocation during decode +6 Precompute cos_w / sin_w once in goertzel Saves two redundant trig calls per invocation (called 2× per bit) +7 x.mul_add(1.0, y) → x + y mul_add with multiplier 1.0 is a no-op identity; clearer without it +8 loop + manual counter → for idx in 0usize.. More idiomatic; + +encoder.rs +# What Why +1 Removed intermediate Vec; iterate &byte → bit shifts directly Eliminates heap allocation of framed.len() * 8 bools +2 Precompute mark_inc / space_inc outside the loop Avoids a multiply + divide per bit (× thousands of bits) +3 silence_len now uses .round() before cast Consistent with every other float→usize conversion in the codebase +4 Removed misleading .max(1) on total_bits Division only reachable inside the loop, which is skipped when total_bits == 0 + +framer.rs +# What Why +1 Added #[derive(Debug, Clone, PartialEq, Eq)] to Decoded Enables idiomatic test assertions, logging, and downstream use +2 CRC bit-by-bit loop → compile-time CRC16_TABLE + single XOR/lookup per byte ~8× fewer branches per byte; table lives in .rodata at zero runtime cost +3 Removed dead .get(..name_len).unwrap_or(name_bytes) → &name_bytes[..name_len] name_len ≤ name_bytes.len() is invariant; dead fallback removed +4 u16::try_from(name_len).unwrap_or(255) → name_len as u16 name_len ≤ 255 by construction; try_from cannot fail, fallback was misleading +5 u32::try_from(data.len()).unwrap_or(u32::MAX) → assert! + direct cast Silent wrong-length write corrupts frames; now panics with clear message +6 Added clippy::cast_possible_truncation to deframe allows u32 as usize can truncate on 16-bit targets; suppresses Clippy false positive + +gui.rs +# What Why +1 UTF-8 safe truncation via char_indices().nth() Byte-offset indexing panics when detail contains multi-byte characters (common in file paths) +2 Extract only the path from dropped_files inside ctx.input() Avoids cloning the entire Vec (including PathBufs and byte buffers) on every single frame +3 Pass filename clone to thread instead of recomputing orig_name Eliminates redundant file_name().to_string_lossy().into_owned() inside the worker +4 detail.to_string() → detail.to_owned() Clippy str_to_string — to_owned() is idiomatic for &str → String + +main.rs +# What Why +1 Gui is now a proper clap Subcommand Visible in --help, validated by clap, cannot accidentally match as a value to another flag +2 Extracted run() → Result<(), String> with ? propagation Single process::exit in main; destructors run on error paths; removes 5 scattered process::exit(1) closures +3 Removed manual std::env::args().any(...) scan No longer needed; eliminates gratuitous String allocation per arg every launch + +wav.rs +# What Why +1 Guard channels == 0 before step_by() step_by(0) panics unconditionally — a malformed WAV would crash the process +2 spec.channels as usize → usize::from(spec.channels) Satisfies Clippy cast_lossless — u16 → usize has a lossless From impl +3 Added TempFile drop guard in tests Temp files are now cleaned up even when assertions fail or the test panics \ No newline at end of file diff --git a/src/decoder.rs b/src/decoder.rs index 2c5ca25..2ecb19e 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -4,17 +4,31 @@ use crate::{ }; use std::f64::consts::TAU; +/// Precomputed sync-word bit pattern (`0x7E 0x7E`), avoiding heap allocation. +#[allow(clippy::indexing_slicing)] // bounds are statically known: byte_idx < 2, bit_idx < 8 +const SYNC_BITS: [bool; 16] = { + let bytes = [0x7E_u8, 0x7E]; + let mut bits = [false; 16]; + let mut byte_idx = 0; + while byte_idx < 2 { + let mut bit_idx = 0; + while bit_idx < 8 { + bits[byte_idx * 8 + bit_idx] = (bytes[byte_idx] >> (7 - bit_idx)) & 1 == 1; + bit_idx += 1; + } + byte_idx += 1; + } + bits +}; + /// Decode PCM samples back to the original filename and payload bytes. -pub fn decode_progress( - samples: &[f64], - on_progress: impl Fn(f32) + Clone, -) -> Result { +pub fn decode_progress(samples: &[f64], on_progress: impl Fn(f32)) -> Result { let spb = f64::from(SAMPLE_RATE) / f64::from(BAUD_RATE); #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let spb_int = spb.round() as usize; for offset in 0..spb_int { - let bits = samples_to_bits(samples, offset, on_progress.clone()); + let bits = samples_to_bits(samples, offset, spb, &on_progress); if let Ok(decoded) = find_frame_in_bits(&bits) { on_progress(1.0); return Ok(decoded); @@ -37,16 +51,20 @@ pub fn decode(samples: &[f64]) -> Result { clippy::cast_precision_loss, // usize → f64 for idx / total: acceptable at these scales clippy::cast_possible_truncation, // f64.round() → usize: always positive integer clippy::cast_sign_loss, // f64.round() → usize: value is always ≥ 0 - clippy::arithmetic_side_effects, // float arithmetic cannot panic - clippy::indexing_slicing, // start..end is bounds-checked by the loop guard above + clippy::arithmetic_side_effects, // float/int arithmetic cannot panic at these magnitudes + clippy::indexing_slicing, // start..end is bounds-checked by the loop guard )] -fn samples_to_bits(samples: &[f64], offset: usize, on_progress: impl Fn(f32)) -> Vec { - let spb = f64::from(SAMPLE_RATE) / f64::from(BAUD_RATE); +fn samples_to_bits( + samples: &[f64], + offset: usize, + spb: f64, + on_progress: &impl Fn(f32), +) -> Vec { let total = samples.len().max(1); - let mut bits = Vec::new(); - let mut idx: usize = 0; + let estimated = (samples.len().saturating_sub(offset)) as f64 / spb; + let mut bits = Vec::with_capacity(estimated as usize); - loop { + for idx in 0usize.. { let start = offset + (idx as f64 * spb).round() as usize; let end = offset + ((idx + 1) as f64 * spb).round() as usize; if end > samples.len() { @@ -56,11 +74,9 @@ fn samples_to_bits(samples: &[f64], offset: usize, on_progress: impl Fn(f32)) -> let w = &samples[start..end]; bits.push(goertzel(w, MARK_FREQ, SAMPLE_RATE) > goertzel(w, SPACE_FREQ, SAMPLE_RATE)); - if idx.is_multiple_of(32) { - #[allow(clippy::cast_precision_loss)] + if idx % 32 == 0 { on_progress(end as f32 / total as f32); } - idx += 1; } bits } @@ -79,17 +95,13 @@ fn samples_to_bits(samples: &[f64], offset: usize, on_progress: impl Fn(f32)) -> clippy::expect_used, // try_into() cannot fail: Vec length is exact )] fn find_frame_in_bits(bits: &[bool]) -> Result { - let sync_bits: Vec = [0x7E_u8, 0x7E] - .iter() - .flat_map(|&b| (0..8u8).rev().map(move |i| (b >> i) & 1 == 1)) - .collect(); - let sync_len = sync_bits.len(); // 16 + let sync_len = SYNC_BITS.len(); // 16 let mut search = 0usize; while search + sync_len <= bits.len() { let Some(rel) = bits[search..] .windows(sync_len) - .position(|w| w == sync_bits.as_slice()) + .position(|w| w == SYNC_BITS.as_slice()) else { break; }; @@ -174,15 +186,17 @@ fn find_frame_in_bits(bits: &[bool]) -> Result { #[allow(clippy::arithmetic_side_effects)] // float arithmetic cannot panic fn goertzel(samples: &[f64], freq: f64, sample_rate: u32) -> f64 { let w = TAU * freq / f64::from(sample_rate); - let coeff = 2.0 * w.cos(); + let cos_w = w.cos(); + let sin_w = w.sin(); + let coeff = 2.0 * cos_w; let (mut s1, mut s2) = (0.0_f64, 0.0_f64); for &x in samples { - let s0 = x.mul_add(1.0, coeff.mul_add(s1, -s2)); + let s0 = x + coeff.mul_add(s1, -s2); s2 = s1; s1 = s0; } - let real = s2.mul_add(-w.cos(), s1); - let imag = s2 * w.sin(); + let real = cos_w.mul_add(-s2, s1); + let imag = sin_w * s2; real.mul_add(real, imag * imag) } diff --git a/src/encoder.rs b/src/encoder.rs index bc49c7f..35ab929 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -7,38 +7,40 @@ use std::f64::consts::TAU; clippy::cast_precision_loss, // usize → f64 for idx / total_bits clippy::cast_possible_truncation, // f64 → usize for silence_len / start / end clippy::cast_sign_loss, // f64.round() → usize (always positive) - clippy::arithmetic_side_effects, // float arithmetic; no panic risk + clippy::arithmetic_side_effects, // float/int arithmetic; no panic risk )] pub fn encode_progress(framed: &[u8], on_progress: impl Fn(f32)) -> Vec { let spb = f64::from(SAMPLE_RATE) / f64::from(BAUD_RATE); + let mark_inc = TAU * MARK_FREQ / f64::from(SAMPLE_RATE); + let space_inc = TAU * SPACE_FREQ / f64::from(SAMPLE_RATE); - let bits: Vec = framed - .iter() - .flat_map(|&byte| (0..8u8).rev().map(move |i| (byte >> i) & 1 == 1)) - .collect(); - - let total_bits = bits.len().max(1); - let silence_len = (f64::from(SAMPLE_RATE) * 0.05) as usize; - let signal_len = (bits.len() as f64 * spb).round() as usize; + let total_bits = framed.len() * 8; + let silence_len = (f64::from(SAMPLE_RATE) * 0.05).round() as usize; + let signal_len = (total_bits as f64 * spb).round() as usize; let mut samples = Vec::with_capacity(silence_len * 2 + signal_len); samples.extend(std::iter::repeat_n(0.0_f64, silence_len)); let mut phase = 0.0_f64; - for (idx, &bit) in bits.iter().enumerate() { - let freq = if bit { MARK_FREQ } else { SPACE_FREQ }; - let phase_inc = TAU * freq / f64::from(SAMPLE_RATE); + let mut bit_idx = 0usize; - let start = (idx as f64 * spb).round() as usize; - let end = ((idx + 1) as f64 * spb).round() as usize; + for &byte in framed { + for i in (0..8u8).rev() { + let bit = (byte >> i) & 1 == 1; + let phase_inc = if bit { mark_inc } else { space_inc }; - for _ in start..end { - samples.push(AMPLITUDE * phase.sin()); - phase = (phase + phase_inc) % TAU; - } + let start = (bit_idx as f64 * spb).round() as usize; + let end = ((bit_idx + 1) as f64 * spb).round() as usize; + + for _ in start..end { + samples.push(AMPLITUDE * phase.sin()); + phase = (phase + phase_inc) % TAU; + } - if idx % 64 == 0 { - on_progress(idx as f32 / total_bits as f32); + if bit_idx.is_multiple_of(64) { + on_progress(bit_idx as f32 / total_bits as f32); + } + bit_idx += 1; } } diff --git a/src/framer.rs b/src/framer.rs index 6368759..d2e9e53 100644 --- a/src/framer.rs +++ b/src/framer.rs @@ -6,16 +6,33 @@ /// ◄──────────────────── CRC covers this span ──────────────────────► use crate::config::{PREAMBLE_LEN, SYNC}; +/// Decoded frame: original filename and payload bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Decoded { + pub filename: String, + pub data: Vec, +} + /// Wrap `data` in a transmittable frame, embedding `filename` so the decoder /// can reconstruct the file with the correct name and extension. +/// +/// # Panics +/// +/// Panics if `data.len()` exceeds `u32::MAX` (≈ 4 GiB). #[allow( - clippy::arithmetic_side_effects, // capacity arithmetic is safe; values are small by construction - clippy::indexing_slicing, // out[PREAMBLE_LEN..] is valid: preamble bytes are always pushed first + clippy::arithmetic_side_effects, // capacity arithmetic is safe; values are small by construction + clippy::cast_possible_truncation, // name_len ≤ 255, data.len ≤ u32::MAX — guarded above each cast + clippy::indexing_slicing, // out[PREAMBLE_LEN..] is valid: preamble bytes are always pushed first )] pub fn frame(data: &[u8], filename: &str) -> Vec { let name_bytes = filename.as_bytes(); let name_len = name_bytes.len().min(255); // cap at 255 bytes - let name_bytes = name_bytes.get(..name_len).unwrap_or(name_bytes); + let name_bytes = &name_bytes[..name_len]; + + assert!( + u32::try_from(data.len()).is_ok(), + "payload exceeds maximum frame size (u32::MAX bytes)" + ); let capacity = PREAMBLE_LEN + 2 + 2 + name_len + 4 + data.len() + 2; let mut out = Vec::with_capacity(capacity); @@ -26,12 +43,12 @@ pub fn frame(data: &[u8], filename: &str) -> Vec { // Sync word out.extend_from_slice(&SYNC); - // Filename length (u16 LE) + filename bytes - out.extend_from_slice(&u16::try_from(name_len).unwrap_or(255).to_le_bytes()); + // Filename length (u16 LE) + filename bytes — name_len ≤ 255 so cast is lossless + out.extend_from_slice(&(name_len as u16).to_le_bytes()); out.extend_from_slice(name_bytes); - // Payload length (u32 LE) + payload - out.extend_from_slice(&u32::try_from(data.len()).unwrap_or(u32::MAX).to_le_bytes()); + // Payload length (u32 LE) + payload — assert above guarantees cast is lossless + out.extend_from_slice(&(data.len() as u32).to_le_bytes()); out.extend_from_slice(data); // CRC-16/CCITT over everything from sync word onwards (not the preamble) @@ -41,20 +58,15 @@ pub fn frame(data: &[u8], filename: &str) -> Vec { out } -/// Decoded frame: original filename and payload bytes. -pub struct Decoded { - pub filename: String, - pub data: Vec, -} - /// Find and validate a frame inside `raw`, returning the embedded filename and payload. /// /// Used only by the byte-level path (tests / CLI verification). /// The audio decoder uses `find_frame_in_bits` in decoder.rs directly. #[allow( dead_code, - clippy::arithmetic_side_effects, // cursor arithmetic is bounds-checked before each use - clippy::indexing_slicing, // all slices are bounds-checked immediately above each access + clippy::arithmetic_side_effects, // cursor arithmetic is bounds-checked before each use + clippy::cast_possible_truncation, // u32 as usize: payload_len is bounds-checked before use + clippy::indexing_slicing, // all slices are bounds-checked immediately above each access )] pub fn deframe(raw: &[u8]) -> Result { let sync_pos = raw @@ -127,18 +139,44 @@ pub fn deframe(raw: &[u8]) -> Result { // CRC-16/CCITT (polynomial 0x1021, init 0xFFFF, no bit-reflection) // --------------------------------------------------------------------------- -#[allow(clippy::arithmetic_side_effects)] // bit-shifting in CRC polynomial; no panic risk -pub fn crc16(data: &[u8]) -> u16 { - let mut crc: u16 = 0xFFFF; - for &byte in data { - crc ^= u16::from(byte) << 8; - for _ in 0..8 { +/// Pre-computed CRC-16/CCITT lookup table (polynomial 0x1021). +#[allow( + clippy::arithmetic_side_effects, + clippy::cast_possible_truncation, + clippy::indexing_slicing +)] +const CRC16_TABLE: [u16; 256] = { + let mut table = [0u16; 256]; + let mut i = 0usize; + while i < 256 { + let mut crc = (i as u16) << 8; + let mut j = 0u8; + while j < 8 { crc = if crc & 0x8000 != 0 { (crc << 1) ^ 0x1021 } else { crc << 1 }; + j += 1; } + table[i] = crc; + i += 1; + } + table +}; + +/// CRC-16/CCITT via table lookup (polynomial 0x1021, init 0xFFFF, no reflection). +#[allow( + clippy::arithmetic_side_effects, + clippy::cast_possible_truncation, + clippy::indexing_slicing +)] +pub fn crc16(data: &[u8]) -> u16 { + let mut crc: u16 = 0xFFFF; + for &byte in data { + // idx is always 0..=255: (u16 >> 8) ^ u16::from(u8) ≤ 0xFF, then cast to u8. + let idx = ((crc >> 8) ^ u16::from(byte)) as u8 as usize; + crc = (crc << 8) ^ CRC16_TABLE[idx]; } crc } diff --git a/src/gui.rs b/src/gui.rs index d311a1d..bd3aac1 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -66,6 +66,7 @@ impl AfskGui { { let progress = Arc::clone(&progress); + let thread_filename = filename.clone(); thread::spawn(move || { let prog = Arc::clone(&progress); let ctx2 = ctx.clone(); @@ -88,12 +89,7 @@ impl AfskGui { std::fs::read(&path) .map_err(|e| e.to_string()) .map(|data| { - let orig_name = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned(); - let framed = crate::framer::frame(&data, &orig_name); + let framed = crate::framer::frame(&data, &thread_filename); crate::encoder::encode_progress(&framed, on_progress) }) .and_then(|samples| { @@ -207,12 +203,12 @@ impl eframe::App for AfskGui { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.poll_worker(); - let dropped = ctx.input(|i| i.raw.dropped_files.clone()); - if let Some(f) = dropped.first() { - if let Some(p) = &f.path { - if !matches!(self.state, State::Processing { .. }) { - self.start_processing(p.clone(), ctx.clone()); - } + // Extract only the path to avoid cloning the entire Vec every frame. + let dropped_path: Option = + ctx.input(|i| i.raw.dropped_files.first().and_then(|f| f.path.clone())); + if let Some(p) = dropped_path { + if !matches!(self.state, State::Processing { .. }) { + self.start_processing(p, ctx.clone()); } } @@ -379,11 +375,15 @@ fn draw_result( color, ); + // Truncate to last ~max_chars characters, safely respecting UTF-8 boundaries. let max_chars = 55usize; - let display = if detail.len() > max_chars { - format!("…{}", &detail[detail.len().saturating_sub(max_chars - 1)..]) + let char_count = detail.chars().count(); + let display = if char_count > max_chars { + let skip = char_count - (max_chars - 1); + let byte_offset = detail.char_indices().nth(skip).map_or(0, |(i, _)| i); + format!("…{}", &detail[byte_offset..]) } else { - detail.to_string() + detail.to_owned() }; painter.text( Pos2::new(cx, cy + 28.0), diff --git a/src/main.rs b/src/main.rs index 2987db7..3483e84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,8 @@ struct Cli { #[derive(Subcommand)] enum Command { + /// Launch the drag-and-drop GUI + Gui, /// Encode a file into an AFSK WAV (original filename is stored in the signal) Encode { #[arg(short, long, value_name = "FILE")] @@ -36,22 +38,23 @@ enum Command { } fn main() { - if std::env::args().any(|a| a == "-gui" || a == "--gui") { - gui::run().unwrap_or_else(|e| { - eprintln!("GUI error: {e}"); - std::process::exit(1); - }); - return; + if let Err(e) = run() { + eprintln!("error: {e}"); + std::process::exit(1); } +} +fn run() -> Result<(), String> { let cli = Cli::parse(); match cli.command { + Command::Gui => { + gui::run().map_err(|e| format!("GUI error: {e}"))?; + } + Command::Encode { input, output } => { - let data = std::fs::read(&input).unwrap_or_else(|e| { - eprintln!("error: cannot read '{}': {e}", input.display()); - std::process::exit(1); - }); + let data = std::fs::read(&input) + .map_err(|e| format!("cannot read '{}': {e}", input.display()))?; let filename = input .file_name() @@ -62,33 +65,25 @@ fn main() { let framed = framer::frame(&data, &filename); let samples = encoder::encode(&framed); - if let Err(e) = wav::write(&output, &samples) { - eprintln!("error: cannot write '{}': {e}", output.display()); - std::process::exit(1); - } + wav::write(&output, &samples) + .map_err(|e| format!("cannot write '{}': {e}", output.display()))?; #[allow(clippy::cast_precision_loss)] let duration = samples.len() as f64 / f64::from(config::SAMPLE_RATE); eprintln!( - "encoded '{}' ({} byte{}) -> {} ({:.2} s)", + "encoded '{}' ({} byte{}) -> {} ({duration:.2} s)", filename, data.len(), plural(data.len()), output.display(), - duration, ); } Command::Decode { input, output } => { - let samples = wav::read(&input).unwrap_or_else(|e| { - eprintln!("error: cannot read '{}': {e}", input.display()); - std::process::exit(1); - }); + let samples = + wav::read(&input).map_err(|e| format!("cannot read '{}': {e}", input.display()))?; - let decoded = decoder::decode(&samples).unwrap_or_else(|e| { - eprintln!("error: decode failed: {e}"); - std::process::exit(1); - }); + let decoded = decoder::decode(&samples).map_err(|e| format!("decode failed: {e}"))?; let out_path = output.unwrap_or_else(|| { input @@ -97,10 +92,8 @@ fn main() { .join(&decoded.filename) }); - if let Err(e) = std::fs::write(&out_path, &decoded.data) { - eprintln!("error: cannot write '{}': {e}", out_path.display()); - std::process::exit(1); - } + std::fs::write(&out_path, &decoded.data) + .map_err(|e| format!("cannot write '{}': {e}", out_path.display()))?; eprintln!( "decoded {} byte{} -> '{}' (original filename: '{}')", @@ -111,6 +104,8 @@ fn main() { ); } } + + Ok(()) } const fn plural(n: usize) -> &'static str { diff --git a/src/wav.rs b/src/wav.rs index b81d3bd..67a6d2a 100644 --- a/src/wav.rs +++ b/src/wav.rs @@ -33,7 +33,10 @@ pub fn read(path: &Path) -> Result, String> { match (spec.bits_per_sample, spec.sample_format) { (16, SampleFormat::Int) => { - let channels = spec.channels as usize; + let channels = usize::from(spec.channels); + if channels == 0 { + return Err("invalid WAV: 0 channels".into()); + } reader .samples::() .step_by(channels) @@ -59,33 +62,47 @@ mod tests { use super::*; use std::f64::consts::TAU; - fn tmp(name: &str) -> std::path::PathBuf { - std::env::temp_dir().join(name) + /// Guard that removes a temp file on drop, even if the test panics. + struct TempFile(std::path::PathBuf); + + impl TempFile { + fn new(name: &str) -> Self { + Self(std::env::temp_dir().join(name)) + } + + fn path(&self) -> &std::path::Path { + &self.0 + } + } + + impl Drop for TempFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } } #[test] fn silence_round_trip() -> Result<(), String> { - let path = tmp("rustwave_wav_silence.wav"); + let tmp = TempFile::new("rustwave_wav_silence.wav"); let original = vec![0.0_f64; 4_410]; - write(&path, &original)?; - let recovered = read(&path)?; + write(tmp.path(), &original)?; + let recovered = read(tmp.path())?; assert_eq!(original.len(), recovered.len()); for v in recovered { assert!(v.abs() < 2.0 / 32_768.0, "expected silence, got {v}"); } - let _ = std::fs::remove_file(&path); Ok(()) } #[test] fn sine_round_trip() -> Result<(), String> { - let path = tmp("rustwave_wav_sine.wav"); + let tmp = TempFile::new("rustwave_wav_sine.wav"); #[allow(clippy::cast_precision_loss)] let original: Vec = (0..44_100_i32) .map(|i| 0.5 * (TAU * 440.0 * f64::from(i) / 44_100.0).sin()) .collect(); - write(&path, &original)?; - let recovered = read(&path)?; + write(tmp.path(), &original)?; + let recovered = read(tmp.path())?; assert_eq!(original.len(), recovered.len()); for (a, b) in original.iter().zip(recovered.iter()) { assert!( @@ -93,7 +110,6 @@ mod tests { "quantisation error too large: {a} vs {b}" ); } - let _ = std::fs::remove_file(&path); Ok(()) } }