Skip to content

Commit a93b8b7

Browse files
authored
Merge pull request #6 from csd113/dev
Check for tests pass
2 parents 22689ac + 08ff7d2 commit a93b8b7

7 files changed

Lines changed: 234 additions & 120 deletions

File tree

changelog.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
CHangelog
2+
3+
## Dev
4+
5+
decoder.rs
6+
# What Why
7+
1 Removed Clone bound; pass on_progress by &impl Fn(f32) Eliminates closure cloning on every phase-offset iteration
8+
2 SYNC_BITS → compile-time const [bool; 16] Eliminates Vec<bool> heap allocation on every call to find_frame_in_bits
9+
3 is_multiple_of(32) → % 32 == 0 is_multiple_of only stabilized in Rust 1.85; modulo is universally supported
10+
4 Pass precomputed spb into samples_to_bits Avoids redundant f64::from / f64::from division
11+
5 Vec::with_capacity(estimated) Pre-allocates the bit vector; avoids repeated reallocation during decode
12+
6 Precompute cos_w / sin_w once in goertzel Saves two redundant trig calls per invocation (called 2× per bit)
13+
7 x.mul_add(1.0, y) → x + y mul_add with multiplier 1.0 is a no-op identity; clearer without it
14+
8 loop + manual counter → for idx in 0usize.. More idiomatic;
15+
16+
encoder.rs
17+
# What Why
18+
1 Removed intermediate Vec<bool>; iterate &byte → bit shifts directly Eliminates heap allocation of framed.len() * 8 bools
19+
2 Precompute mark_inc / space_inc outside the loop Avoids a multiply + divide per bit (× thousands of bits)
20+
3 silence_len now uses .round() before cast Consistent with every other float→usize conversion in the codebase
21+
4 Removed misleading .max(1) on total_bits Division only reachable inside the loop, which is skipped when total_bits == 0
22+
23+
framer.rs
24+
# What Why
25+
1 Added #[derive(Debug, Clone, PartialEq, Eq)] to Decoded Enables idiomatic test assertions, logging, and downstream use
26+
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
27+
3 Removed dead .get(..name_len).unwrap_or(name_bytes) → &name_bytes[..name_len] name_len ≤ name_bytes.len() is invariant; dead fallback removed
28+
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
29+
5 u32::try_from(data.len()).unwrap_or(u32::MAX) → assert! + direct cast Silent wrong-length write corrupts frames; now panics with clear message
30+
6 Added clippy::cast_possible_truncation to deframe allows u32 as usize can truncate on 16-bit targets; suppresses Clippy false positive
31+
32+
gui.rs
33+
# What Why
34+
1 UTF-8 safe truncation via char_indices().nth() Byte-offset indexing panics when detail contains multi-byte characters (common in file paths)
35+
2 Extract only the path from dropped_files inside ctx.input() Avoids cloning the entire Vec<DroppedFile> (including PathBufs and byte buffers) on every single frame
36+
3 Pass filename clone to thread instead of recomputing orig_name Eliminates redundant file_name().to_string_lossy().into_owned() inside the worker
37+
4 detail.to_string() → detail.to_owned() Clippy str_to_string — to_owned() is idiomatic for &str → String
38+
39+
main.rs
40+
# What Why
41+
1 Gui is now a proper clap Subcommand Visible in --help, validated by clap, cannot accidentally match as a value to another flag
42+
2 Extracted run() → Result<(), String> with ? propagation Single process::exit in main; destructors run on error paths; removes 5 scattered process::exit(1) closures
43+
3 Removed manual std::env::args().any(...) scan No longer needed; eliminates gratuitous String allocation per arg every launch
44+
45+
wav.rs
46+
# What Why
47+
1 Guard channels == 0 before step_by() step_by(0) panics unconditionally — a malformed WAV would crash the process
48+
2 spec.channels as usize → usize::from(spec.channels) Satisfies Clippy cast_lossless — u16 → usize has a lossless From impl
49+
3 Added TempFile drop guard in tests Temp files are now cleaned up even when assertions fail or the test panics

src/decoder.rs

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@ use crate::{
44
};
55
use std::f64::consts::TAU;
66

7+
/// Precomputed sync-word bit pattern (`0x7E 0x7E`), avoiding heap allocation.
8+
#[allow(clippy::indexing_slicing)] // bounds are statically known: byte_idx < 2, bit_idx < 8
9+
const SYNC_BITS: [bool; 16] = {
10+
let bytes = [0x7E_u8, 0x7E];
11+
let mut bits = [false; 16];
12+
let mut byte_idx = 0;
13+
while byte_idx < 2 {
14+
let mut bit_idx = 0;
15+
while bit_idx < 8 {
16+
bits[byte_idx * 8 + bit_idx] = (bytes[byte_idx] >> (7 - bit_idx)) & 1 == 1;
17+
bit_idx += 1;
18+
}
19+
byte_idx += 1;
20+
}
21+
bits
22+
};
23+
724
/// Decode PCM samples back to the original filename and payload bytes.
8-
pub fn decode_progress(
9-
samples: &[f64],
10-
on_progress: impl Fn(f32) + Clone,
11-
) -> Result<Decoded, String> {
25+
pub fn decode_progress(samples: &[f64], on_progress: impl Fn(f32)) -> Result<Decoded, String> {
1226
let spb = f64::from(SAMPLE_RATE) / f64::from(BAUD_RATE);
1327
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1428
let spb_int = spb.round() as usize;
1529

1630
for offset in 0..spb_int {
17-
let bits = samples_to_bits(samples, offset, on_progress.clone());
31+
let bits = samples_to_bits(samples, offset, spb, &on_progress);
1832
if let Ok(decoded) = find_frame_in_bits(&bits) {
1933
on_progress(1.0);
2034
return Ok(decoded);
@@ -37,16 +51,20 @@ pub fn decode(samples: &[f64]) -> Result<Decoded, String> {
3751
clippy::cast_precision_loss, // usize → f64 for idx / total: acceptable at these scales
3852
clippy::cast_possible_truncation, // f64.round() → usize: always positive integer
3953
clippy::cast_sign_loss, // f64.round() → usize: value is always ≥ 0
40-
clippy::arithmetic_side_effects, // float arithmetic cannot panic
41-
clippy::indexing_slicing, // start..end is bounds-checked by the loop guard above
54+
clippy::arithmetic_side_effects, // float/int arithmetic cannot panic at these magnitudes
55+
clippy::indexing_slicing, // start..end is bounds-checked by the loop guard
4256
)]
43-
fn samples_to_bits(samples: &[f64], offset: usize, on_progress: impl Fn(f32)) -> Vec<bool> {
44-
let spb = f64::from(SAMPLE_RATE) / f64::from(BAUD_RATE);
57+
fn samples_to_bits(
58+
samples: &[f64],
59+
offset: usize,
60+
spb: f64,
61+
on_progress: &impl Fn(f32),
62+
) -> Vec<bool> {
4563
let total = samples.len().max(1);
46-
let mut bits = Vec::new();
47-
let mut idx: usize = 0;
64+
let estimated = (samples.len().saturating_sub(offset)) as f64 / spb;
65+
let mut bits = Vec::with_capacity(estimated as usize);
4866

49-
loop {
67+
for idx in 0usize.. {
5068
let start = offset + (idx as f64 * spb).round() as usize;
5169
let end = offset + ((idx + 1) as f64 * spb).round() as usize;
5270
if end > samples.len() {
@@ -56,11 +74,9 @@ fn samples_to_bits(samples: &[f64], offset: usize, on_progress: impl Fn(f32)) ->
5674
let w = &samples[start..end];
5775
bits.push(goertzel(w, MARK_FREQ, SAMPLE_RATE) > goertzel(w, SPACE_FREQ, SAMPLE_RATE));
5876

59-
if idx.is_multiple_of(32) {
60-
#[allow(clippy::cast_precision_loss)]
77+
if idx % 32 == 0 {
6178
on_progress(end as f32 / total as f32);
6279
}
63-
idx += 1;
6480
}
6581
bits
6682
}
@@ -79,17 +95,13 @@ fn samples_to_bits(samples: &[f64], offset: usize, on_progress: impl Fn(f32)) ->
7995
clippy::expect_used, // try_into() cannot fail: Vec length is exact
8096
)]
8197
fn find_frame_in_bits(bits: &[bool]) -> Result<Decoded, String> {
82-
let sync_bits: Vec<bool> = [0x7E_u8, 0x7E]
83-
.iter()
84-
.flat_map(|&b| (0..8u8).rev().map(move |i| (b >> i) & 1 == 1))
85-
.collect();
86-
let sync_len = sync_bits.len(); // 16
98+
let sync_len = SYNC_BITS.len(); // 16
8799

88100
let mut search = 0usize;
89101
while search + sync_len <= bits.len() {
90102
let Some(rel) = bits[search..]
91103
.windows(sync_len)
92-
.position(|w| w == sync_bits.as_slice())
104+
.position(|w| w == SYNC_BITS.as_slice())
93105
else {
94106
break;
95107
};
@@ -174,15 +186,17 @@ fn find_frame_in_bits(bits: &[bool]) -> Result<Decoded, String> {
174186
#[allow(clippy::arithmetic_side_effects)] // float arithmetic cannot panic
175187
fn goertzel(samples: &[f64], freq: f64, sample_rate: u32) -> f64 {
176188
let w = TAU * freq / f64::from(sample_rate);
177-
let coeff = 2.0 * w.cos();
189+
let cos_w = w.cos();
190+
let sin_w = w.sin();
191+
let coeff = 2.0 * cos_w;
178192
let (mut s1, mut s2) = (0.0_f64, 0.0_f64);
179193
for &x in samples {
180-
let s0 = x.mul_add(1.0, coeff.mul_add(s1, -s2));
194+
let s0 = x + coeff.mul_add(s1, -s2);
181195
s2 = s1;
182196
s1 = s0;
183197
}
184-
let real = s2.mul_add(-w.cos(), s1);
185-
let imag = s2 * w.sin();
198+
let real = cos_w.mul_add(-s2, s1);
199+
let imag = sin_w * s2;
186200
real.mul_add(real, imag * imag)
187201
}
188202

src/encoder.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,40 @@ use std::f64::consts::TAU;
77
clippy::cast_precision_loss, // usize → f64 for idx / total_bits
88
clippy::cast_possible_truncation, // f64 → usize for silence_len / start / end
99
clippy::cast_sign_loss, // f64.round() → usize (always positive)
10-
clippy::arithmetic_side_effects, // float arithmetic; no panic risk
10+
clippy::arithmetic_side_effects, // float/int arithmetic; no panic risk
1111
)]
1212
pub fn encode_progress(framed: &[u8], on_progress: impl Fn(f32)) -> Vec<f64> {
1313
let spb = f64::from(SAMPLE_RATE) / f64::from(BAUD_RATE);
14+
let mark_inc = TAU * MARK_FREQ / f64::from(SAMPLE_RATE);
15+
let space_inc = TAU * SPACE_FREQ / f64::from(SAMPLE_RATE);
1416

15-
let bits: Vec<bool> = framed
16-
.iter()
17-
.flat_map(|&byte| (0..8u8).rev().map(move |i| (byte >> i) & 1 == 1))
18-
.collect();
19-
20-
let total_bits = bits.len().max(1);
21-
let silence_len = (f64::from(SAMPLE_RATE) * 0.05) as usize;
22-
let signal_len = (bits.len() as f64 * spb).round() as usize;
17+
let total_bits = framed.len() * 8;
18+
let silence_len = (f64::from(SAMPLE_RATE) * 0.05).round() as usize;
19+
let signal_len = (total_bits as f64 * spb).round() as usize;
2320
let mut samples = Vec::with_capacity(silence_len * 2 + signal_len);
2421

2522
samples.extend(std::iter::repeat_n(0.0_f64, silence_len));
2623

2724
let mut phase = 0.0_f64;
28-
for (idx, &bit) in bits.iter().enumerate() {
29-
let freq = if bit { MARK_FREQ } else { SPACE_FREQ };
30-
let phase_inc = TAU * freq / f64::from(SAMPLE_RATE);
25+
let mut bit_idx = 0usize;
3126

32-
let start = (idx as f64 * spb).round() as usize;
33-
let end = ((idx + 1) as f64 * spb).round() as usize;
27+
for &byte in framed {
28+
for i in (0..8u8).rev() {
29+
let bit = (byte >> i) & 1 == 1;
30+
let phase_inc = if bit { mark_inc } else { space_inc };
3431

35-
for _ in start..end {
36-
samples.push(AMPLITUDE * phase.sin());
37-
phase = (phase + phase_inc) % TAU;
38-
}
32+
let start = (bit_idx as f64 * spb).round() as usize;
33+
let end = ((bit_idx + 1) as f64 * spb).round() as usize;
34+
35+
for _ in start..end {
36+
samples.push(AMPLITUDE * phase.sin());
37+
phase = (phase + phase_inc) % TAU;
38+
}
3939

40-
if idx % 64 == 0 {
41-
on_progress(idx as f32 / total_bits as f32);
40+
if bit_idx.is_multiple_of(64) {
41+
on_progress(bit_idx as f32 / total_bits as f32);
42+
}
43+
bit_idx += 1;
4244
}
4345
}
4446

src/framer.rs

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,33 @@
66
/// ◄──────────────────── CRC covers this span ──────────────────────►
77
use crate::config::{PREAMBLE_LEN, SYNC};
88

9+
/// Decoded frame: original filename and payload bytes.
10+
#[derive(Debug, Clone, PartialEq, Eq)]
11+
pub struct Decoded {
12+
pub filename: String,
13+
pub data: Vec<u8>,
14+
}
15+
916
/// Wrap `data` in a transmittable frame, embedding `filename` so the decoder
1017
/// can reconstruct the file with the correct name and extension.
18+
///
19+
/// # Panics
20+
///
21+
/// Panics if `data.len()` exceeds `u32::MAX` (≈ 4 GiB).
1122
#[allow(
12-
clippy::arithmetic_side_effects, // capacity arithmetic is safe; values are small by construction
13-
clippy::indexing_slicing, // out[PREAMBLE_LEN..] is valid: preamble bytes are always pushed first
23+
clippy::arithmetic_side_effects, // capacity arithmetic is safe; values are small by construction
24+
clippy::cast_possible_truncation, // name_len ≤ 255, data.len ≤ u32::MAX — guarded above each cast
25+
clippy::indexing_slicing, // out[PREAMBLE_LEN..] is valid: preamble bytes are always pushed first
1426
)]
1527
pub fn frame(data: &[u8], filename: &str) -> Vec<u8> {
1628
let name_bytes = filename.as_bytes();
1729
let name_len = name_bytes.len().min(255); // cap at 255 bytes
18-
let name_bytes = name_bytes.get(..name_len).unwrap_or(name_bytes);
30+
let name_bytes = &name_bytes[..name_len];
31+
32+
assert!(
33+
u32::try_from(data.len()).is_ok(),
34+
"payload exceeds maximum frame size (u32::MAX bytes)"
35+
);
1936

2037
let capacity = PREAMBLE_LEN + 2 + 2 + name_len + 4 + data.len() + 2;
2138
let mut out = Vec::with_capacity(capacity);
@@ -26,12 +43,12 @@ pub fn frame(data: &[u8], filename: &str) -> Vec<u8> {
2643
// Sync word
2744
out.extend_from_slice(&SYNC);
2845

29-
// Filename length (u16 LE) + filename bytes
30-
out.extend_from_slice(&u16::try_from(name_len).unwrap_or(255).to_le_bytes());
46+
// Filename length (u16 LE) + filename bytes — name_len ≤ 255 so cast is lossless
47+
out.extend_from_slice(&(name_len as u16).to_le_bytes());
3148
out.extend_from_slice(name_bytes);
3249

33-
// Payload length (u32 LE) + payload
34-
out.extend_from_slice(&u32::try_from(data.len()).unwrap_or(u32::MAX).to_le_bytes());
50+
// Payload length (u32 LE) + payload — assert above guarantees cast is lossless
51+
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
3552
out.extend_from_slice(data);
3653

3754
// CRC-16/CCITT over everything from sync word onwards (not the preamble)
@@ -41,20 +58,15 @@ pub fn frame(data: &[u8], filename: &str) -> Vec<u8> {
4158
out
4259
}
4360

44-
/// Decoded frame: original filename and payload bytes.
45-
pub struct Decoded {
46-
pub filename: String,
47-
pub data: Vec<u8>,
48-
}
49-
5061
/// Find and validate a frame inside `raw`, returning the embedded filename and payload.
5162
///
5263
/// Used only by the byte-level path (tests / CLI verification).
5364
/// The audio decoder uses `find_frame_in_bits` in decoder.rs directly.
5465
#[allow(
5566
dead_code,
56-
clippy::arithmetic_side_effects, // cursor arithmetic is bounds-checked before each use
57-
clippy::indexing_slicing, // all slices are bounds-checked immediately above each access
67+
clippy::arithmetic_side_effects, // cursor arithmetic is bounds-checked before each use
68+
clippy::cast_possible_truncation, // u32 as usize: payload_len is bounds-checked before use
69+
clippy::indexing_slicing, // all slices are bounds-checked immediately above each access
5870
)]
5971
pub fn deframe(raw: &[u8]) -> Result<Decoded, String> {
6072
let sync_pos = raw
@@ -127,18 +139,44 @@ pub fn deframe(raw: &[u8]) -> Result<Decoded, String> {
127139
// CRC-16/CCITT (polynomial 0x1021, init 0xFFFF, no bit-reflection)
128140
// ---------------------------------------------------------------------------
129141

130-
#[allow(clippy::arithmetic_side_effects)] // bit-shifting in CRC polynomial; no panic risk
131-
pub fn crc16(data: &[u8]) -> u16 {
132-
let mut crc: u16 = 0xFFFF;
133-
for &byte in data {
134-
crc ^= u16::from(byte) << 8;
135-
for _ in 0..8 {
142+
/// Pre-computed CRC-16/CCITT lookup table (polynomial 0x1021).
143+
#[allow(
144+
clippy::arithmetic_side_effects,
145+
clippy::cast_possible_truncation,
146+
clippy::indexing_slicing
147+
)]
148+
const CRC16_TABLE: [u16; 256] = {
149+
let mut table = [0u16; 256];
150+
let mut i = 0usize;
151+
while i < 256 {
152+
let mut crc = (i as u16) << 8;
153+
let mut j = 0u8;
154+
while j < 8 {
136155
crc = if crc & 0x8000 != 0 {
137156
(crc << 1) ^ 0x1021
138157
} else {
139158
crc << 1
140159
};
160+
j += 1;
141161
}
162+
table[i] = crc;
163+
i += 1;
164+
}
165+
table
166+
};
167+
168+
/// CRC-16/CCITT via table lookup (polynomial 0x1021, init 0xFFFF, no reflection).
169+
#[allow(
170+
clippy::arithmetic_side_effects,
171+
clippy::cast_possible_truncation,
172+
clippy::indexing_slicing
173+
)]
174+
pub fn crc16(data: &[u8]) -> u16 {
175+
let mut crc: u16 = 0xFFFF;
176+
for &byte in data {
177+
// idx is always 0..=255: (u16 >> 8) ^ u16::from(u8) ≤ 0xFF, then cast to u8.
178+
let idx = ((crc >> 8) ^ u16::from(byte)) as u8 as usize;
179+
crc = (crc << 8) ^ CRC16_TABLE[idx];
142180
}
143181
crc
144182
}

0 commit comments

Comments
 (0)