1use std::io::{self, Error, ErrorKind, Read};
38use std::path::Path;
39
40#[derive(Clone, Debug)]
43pub struct EdfSignal {
44 pub fs: f64,
46 pub channels: Vec<Vec<i64>>,
48 pub labels: Vec<String>,
50}
51
52const HEADER_BLOCK: usize = 256;
54const ANNOTATION_LABEL: &str = "EDF Annotations";
56
57fn take<'a>(buf: &'a [u8], pos: &mut usize, len: usize) -> io::Result<&'a [u8]> {
60 let end = pos
61 .checked_add(len)
62 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "EDF header offset overflow"))?;
63 if end > buf.len() {
64 return Err(Error::new(
65 ErrorKind::UnexpectedEof,
66 "EDF header truncated: not enough bytes for field",
67 ));
68 }
69 let field = &buf[*pos..end];
70 *pos = end;
71 Ok(field)
72}
73
74fn field_str(bytes: &[u8]) -> String {
78 String::from_utf8_lossy(bytes).trim().to_string()
79}
80
81fn parse_i64(bytes: &[u8], what: &str) -> io::Result<i64> {
83 let s = field_str(bytes);
84 s.parse::<i64>().map_err(|_| {
85 Error::new(
86 ErrorKind::InvalidData,
87 format!("EDF: malformed integer field {what:?}: {s:?}"),
88 )
89 })
90}
91
92fn parse_f64(bytes: &[u8], what: &str) -> io::Result<f64> {
94 let s = field_str(bytes);
95 s.parse::<f64>().map_err(|_| {
96 Error::new(
97 ErrorKind::InvalidData,
98 format!("EDF: malformed float field {what:?}: {s:?}"),
99 )
100 })
101}
102
103pub fn read_edf<P: AsRef<Path>>(path: P) -> io::Result<EdfSignal> {
112 let mut buf = Vec::new();
113 std::fs::File::open(path.as_ref())?.read_to_end(&mut buf)?;
114
115 if buf.len() < HEADER_BLOCK {
117 return Err(Error::new(
118 ErrorKind::UnexpectedEof,
119 "EDF file shorter than 256-byte main header",
120 ));
121 }
122 let mut pos = 0usize;
123 let _version = take(&buf, &mut pos, 8)?;
124 let _patient = take(&buf, &mut pos, 80)?;
125 let _recording = take(&buf, &mut pos, 80)?;
126 let _startdate = take(&buf, &mut pos, 8)?;
127 let _starttime = take(&buf, &mut pos, 8)?;
128 let header_bytes = parse_i64(take(&buf, &mut pos, 8)?, "header_bytes")?;
129 let _reserved = take(&buf, &mut pos, 44)?;
130 let n_records = parse_i64(take(&buf, &mut pos, 8)?, "n_data_records")?;
131 let record_duration = parse_f64(take(&buf, &mut pos, 8)?, "record_duration_sec")?;
132 let ns = parse_i64(take(&buf, &mut pos, 4)?, "n_signals")?;
133
134 if ns < 0 {
135 return Err(Error::new(
136 ErrorKind::InvalidData,
137 "EDF: negative signal count",
138 ));
139 }
140 let ns = ns as usize;
141 if ns == 0 {
142 return Err(Error::new(
143 ErrorKind::InvalidData,
144 "EDF: file declares zero signals",
145 ));
146 }
147 if record_duration <= 0.0 || !record_duration.is_finite() {
148 return Err(Error::new(
149 ErrorKind::InvalidData,
150 "EDF: non-positive record duration",
151 ));
152 }
153 if n_records < 0 {
156 return Err(Error::new(
157 ErrorKind::InvalidData,
158 "EDF: unknown/negative data-record count not supported",
159 ));
160 }
161 let n_records = n_records as usize;
162
163 let expected_header = HEADER_BLOCK
166 .checked_add(ns.checked_mul(HEADER_BLOCK).ok_or_else(|| {
167 Error::new(ErrorKind::InvalidData, "EDF: signal count overflows header")
168 })?)
169 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "EDF: header size overflow"))?;
170 if header_bytes < expected_header as i64 {
171 return Err(Error::new(
172 ErrorKind::InvalidData,
173 "EDF: declared header_bytes smaller than ns*256+256",
174 ));
175 }
176 let header_bytes = header_bytes as usize;
177
178 let labels_raw = take(&buf, &mut pos, ns * 16)?.to_vec();
182 let _transducer = take(&buf, &mut pos, ns * 80)?;
183 let _phys_dim = take(&buf, &mut pos, ns * 8)?;
184 let _phys_min = take(&buf, &mut pos, ns * 8)?;
185 let _phys_max = take(&buf, &mut pos, ns * 8)?;
186 let _dig_min = take(&buf, &mut pos, ns * 8)?;
187 let _dig_max = take(&buf, &mut pos, ns * 8)?;
188 let _prefilter = take(&buf, &mut pos, ns * 80)?;
189 let nsamp_raw = take(&buf, &mut pos, ns * 8)?.to_vec();
190 let _sig_reserved = take(&buf, &mut pos, ns * 32)?;
191
192 let mut labels = Vec::with_capacity(ns);
193 let mut samples_per_record = Vec::with_capacity(ns);
194 for i in 0..ns {
195 labels.push(field_str(&labels_raw[i * 16..i * 16 + 16]));
196 let n = parse_i64(&nsamp_raw[i * 8..i * 8 + 8], "n_samples_per_record")?;
197 if n < 0 {
198 return Err(Error::new(
199 ErrorKind::InvalidData,
200 "EDF: negative n_samples_per_record",
201 ));
202 }
203 samples_per_record.push(n as usize);
204 }
205
206 let record_samples: usize = samples_per_record
208 .iter()
209 .try_fold(0usize, |acc, &n| acc.checked_add(n))
210 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "EDF: record sample count overflow"))?;
211 let record_bytes = record_samples
212 .checked_mul(2)
213 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "EDF: record byte size overflow"))?;
214 let data_bytes = record_bytes
215 .checked_mul(n_records)
216 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "EDF: data section size overflow"))?;
217
218 let data_start = header_bytes;
221 let data_end = data_start
222 .checked_add(data_bytes)
223 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "EDF: data section end overflow"))?;
224 if data_end > buf.len() {
225 return Err(Error::new(
226 ErrorKind::UnexpectedEof,
227 "EDF: data section truncated (fewer bytes than declared records)",
228 ));
229 }
230
231 let mut channels: Vec<Vec<i64>> = samples_per_record
233 .iter()
234 .map(|&n| Vec::with_capacity(n.saturating_mul(n_records)))
235 .collect();
236
237 let mut cursor = data_start;
240 for _rec in 0..n_records {
241 for (sig, &n) in samples_per_record.iter().enumerate() {
242 let chan = &mut channels[sig];
243 for _ in 0..n {
244 let lo = buf[cursor];
247 let hi = buf[cursor + 1];
248 let val = i16::from_le_bytes([lo, hi]) as i64;
249 chan.push(val);
250 cursor += 2;
251 }
252 }
253 }
254
255 let mut out_channels = Vec::new();
257 let mut out_labels = Vec::new();
258 let mut ref_fs: Option<f64> = None;
259
260 for i in 0..ns {
261 if labels[i] == ANNOTATION_LABEL {
262 continue;
263 }
264 let fs = samples_per_record[i] as f64 / record_duration;
265 match ref_fs {
266 None => ref_fs = Some(fs),
267 Some(r) => {
268 if (fs - r).abs() > 1e-9 {
269 return Err(Error::new(
270 ErrorKind::InvalidData,
271 format!(
272 "EDF: non-uniform sampling rates ({r} Hz vs {fs} Hz on \
273 {label:?}); OpenECS requires one shared fs",
274 label = labels[i]
275 ),
276 ));
277 }
278 }
279 }
280 out_channels.push(std::mem::take(&mut channels[i]));
281 out_labels.push(labels[i].clone());
282 }
283
284 let fs = ref_fs.ok_or_else(|| {
285 Error::new(
286 ErrorKind::InvalidData,
287 "EDF: no signal channels (all dropped as annotations)",
288 )
289 })?;
290
291 Ok(EdfSignal {
292 fs,
293 channels: out_channels,
294 labels: out_labels,
295 })
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use std::io::Write;
302
303 fn ascii_field(out: &mut Vec<u8>, value: &str, width: usize) {
306 let bytes = value.as_bytes();
307 assert!(bytes.len() <= width, "test field {value:?} exceeds width");
308 out.extend_from_slice(bytes);
309 out.extend(std::iter::repeat(b' ').take(width - bytes.len()));
310 }
311
312 fn build_edf(labels: &[&str], samples_per_record: usize, records: &[Vec<i16>]) -> Vec<u8> {
318 let ns = labels.len();
319 let n_records = if ns == 0 { 0 } else { records.len() / ns };
320 let header_bytes = HEADER_BLOCK + ns * HEADER_BLOCK;
321
322 let mut buf = Vec::new();
323 ascii_field(&mut buf, "0", 8); ascii_field(&mut buf, "X X X X", 80); ascii_field(&mut buf, "Startdate", 80); ascii_field(&mut buf, "01.01.26", 8); ascii_field(&mut buf, "00.00.00", 8); ascii_field(&mut buf, &header_bytes.to_string(), 8); ascii_field(&mut buf, "", 44); ascii_field(&mut buf, &n_records.to_string(), 8); ascii_field(&mut buf, "1", 8); ascii_field(&mut buf, &ns.to_string(), 4); for &l in labels {
337 ascii_field(&mut buf, l, 16);
338 }
339 for _ in 0..ns {
340 ascii_field(&mut buf, "AgAgCl", 80); }
342 for _ in 0..ns {
343 ascii_field(&mut buf, "uV", 8); }
345 for _ in 0..ns {
346 ascii_field(&mut buf, "-32768", 8); }
348 for _ in 0..ns {
349 ascii_field(&mut buf, "32767", 8); }
351 for _ in 0..ns {
352 ascii_field(&mut buf, "-32768", 8); }
354 for _ in 0..ns {
355 ascii_field(&mut buf, "32767", 8); }
357 for _ in 0..ns {
358 ascii_field(&mut buf, "HP:0.1Hz", 80); }
360 for _ in 0..ns {
361 ascii_field(&mut buf, &samples_per_record.to_string(), 8); }
363 for _ in 0..ns {
364 ascii_field(&mut buf, "", 32); }
366 assert_eq!(buf.len(), header_bytes, "header block size mismatch");
367
368 for block in records {
371 for &s in block {
372 buf.extend_from_slice(&s.to_le_bytes());
373 }
374 }
375 buf
376 }
377
378 fn write_tempfile(tag: &str, bytes: &[u8]) -> std::path::PathBuf {
380 let nanos = std::time::SystemTime::now()
382 .duration_since(std::time::UNIX_EPOCH)
383 .map(|d| d.as_nanos())
384 .unwrap_or(0);
385 let name = format!("lqs_edf_test_{}_{}_{}.edf", std::process::id(), nanos, tag);
386 let path = std::env::temp_dir().join(name);
387 let mut f = std::fs::File::create(&path).expect("create tempfile");
388 f.write_all(bytes).expect("write tempfile");
389 f.flush().expect("flush tempfile");
390 path
391 }
392
393 #[test]
394 fn roundtrip_two_signals_two_records() {
395 let sig0_r0: Vec<i16> = vec![1, 2, 3];
399 let sig1_r0: Vec<i16> = vec![-1, -2, -3];
400 let sig0_r1: Vec<i16> = vec![100, 200, 300];
401 let sig1_r1: Vec<i16> = vec![-100, i16::MIN, i16::MAX];
402 let records = vec![
403 sig0_r0.clone(),
404 sig1_r0.clone(),
405 sig0_r1.clone(),
406 sig1_r1.clone(),
407 ];
408 let bytes = build_edf(&["Fp1", "Fp2"], 3, &records);
409 let path = write_tempfile("rt2x2", &bytes);
410
411 let result = read_edf(&path);
412 let _ = std::fs::remove_file(&path);
414 let edf = result.expect("read_edf must succeed on valid fixture");
415
416 assert_eq!(edf.channels.len(), 2, "two kept channels");
417 assert_eq!(edf.labels, vec!["Fp1".to_string(), "Fp2".to_string()]);
418 assert_eq!(edf.fs, 3.0, "fs = samples_per_record / duration");
419
420 let expect0: Vec<i64> = sig0_r0
422 .iter()
423 .chain(sig0_r1.iter())
424 .map(|&v| v as i64)
425 .collect();
426 let expect1: Vec<i64> = sig1_r0
427 .iter()
428 .chain(sig1_r1.iter())
429 .map(|&v| v as i64)
430 .collect();
431 assert_eq!(edf.channels[0], expect0, "channel 0 digital samples");
432 assert_eq!(edf.channels[1], expect1, "channel 1 digital samples");
433 }
434
435 #[test]
436 fn drops_annotation_channel() {
437 let real_r0: Vec<i16> = vec![10, 20];
440 let anno_r0: Vec<i16> = vec![0, 0];
441 let real_r1: Vec<i16> = vec![30, 40];
442 let anno_r1: Vec<i16> = vec![0, 0];
443 let records = vec![real_r0, anno_r0, real_r1, anno_r1];
444 let bytes = build_edf(&["C3", "EDF Annotations"], 2, &records);
445 let path = write_tempfile("anno", &bytes);
446
447 let result = read_edf(&path);
448 let _ = std::fs::remove_file(&path);
449 let edf = result.expect("read_edf must succeed");
450
451 assert_eq!(edf.channels.len(), 1, "annotation channel dropped");
452 assert_eq!(edf.labels, vec!["C3".to_string()]);
453 assert_eq!(edf.fs, 2.0);
454 assert_eq!(edf.channels[0], vec![10i64, 20, 30, 40]);
455 }
456
457 #[test]
458 fn truncated_data_is_error_not_panic() {
459 let records = vec![vec![1i16, 2, 3], vec![4, 5, 6]];
460 let mut bytes = build_edf(&["Fz"], 3, &records);
461 bytes.truncate(bytes.len() - 5);
463 let path = write_tempfile("trunc", &bytes);
464
465 let result = read_edf(&path);
466 let _ = std::fs::remove_file(&path);
467 assert!(result.is_err(), "truncated data must return io::Error");
468 }
469
470 #[test]
471 fn short_header_is_error_not_panic() {
472 let path = write_tempfile("short", &[b'0'; 10]);
473 let result = read_edf(&path);
474 let _ = std::fs::remove_file(&path);
475 assert!(result.is_err(), "sub-header file must return io::Error");
476 }
477
478 #[test]
479 fn nonuniform_rates_rejected() {
480 let ns = 2;
484 let header_bytes = HEADER_BLOCK + ns * HEADER_BLOCK;
485 let mut buf = Vec::new();
486 ascii_field(&mut buf, "0", 8);
487 ascii_field(&mut buf, "X", 80);
488 ascii_field(&mut buf, "R", 80);
489 ascii_field(&mut buf, "01.01.26", 8);
490 ascii_field(&mut buf, "00.00.00", 8);
491 ascii_field(&mut buf, &header_bytes.to_string(), 8);
492 ascii_field(&mut buf, "", 44);
493 ascii_field(&mut buf, "1", 8); ascii_field(&mut buf, "1", 8); ascii_field(&mut buf, &ns.to_string(), 4);
496 ascii_field(&mut buf, "A", 16);
497 ascii_field(&mut buf, "B", 16);
498 for _ in 0..ns {
499 ascii_field(&mut buf, "T", 80);
500 }
501 for _ in 0..ns {
502 ascii_field(&mut buf, "uV", 8);
503 }
504 for _ in 0..ns {
505 ascii_field(&mut buf, "-1", 8);
506 }
507 for _ in 0..ns {
508 ascii_field(&mut buf, "1", 8);
509 }
510 for _ in 0..ns {
511 ascii_field(&mut buf, "-1", 8);
512 }
513 for _ in 0..ns {
514 ascii_field(&mut buf, "1", 8);
515 }
516 for _ in 0..ns {
517 ascii_field(&mut buf, "P", 80);
518 }
519 ascii_field(&mut buf, "2", 8); ascii_field(&mut buf, "4", 8); for _ in 0..ns {
522 ascii_field(&mut buf, "", 32);
523 }
524 for s in [1i16, 2] {
526 buf.extend_from_slice(&s.to_le_bytes());
527 }
528 for s in [3i16, 4, 5, 6] {
529 buf.extend_from_slice(&s.to_le_bytes());
530 }
531 let path = write_tempfile("nonuniform", &buf);
532
533 let result = read_edf(&path);
534 let _ = std::fs::remove_file(&path);
535 assert!(result.is_err(), "non-uniform fs must be rejected");
536 }
537}