From b26c633180deff88c040fe57ac543c79ffd0d53c Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Tue, 9 Jun 2026 03:32:56 +0000 Subject: [PATCH 1/9] test: pin ATDF timestamp and empty-KxCf behavior Cover two previously untested paths: ATDF time fields render a fixed UTC string, and read_kx_cf with k=0 returns an empty vec without moving the cursor. --- src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 0212fe0..462c853 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -709,6 +709,14 @@ mod tests { stdf_types::read_kx_cf(&raw_data, &mut pos, 4, 0) ); assert_eq!(pos, 3); + // k == 0 returns an empty vec without moving the cursor. + // Start from a distinct pos to prove non-advancement. + let mut pos = 5; + assert_eq!( + Vec::::new(), + stdf_types::read_kx_cf(&raw_data, &mut pos, 0, 0) + ); + assert_eq!(pos, 5); } #[test] @@ -887,6 +895,17 @@ mod tests { #[cfg(feature = "atdf")] use atdf_types::atdf_record_field::*; + #[cfg(feature = "atdf")] + #[test] + fn test_atdf_timestamp_is_utc() { + // A fixed UTC instant, non-midnight so a local-tz or zeroed-time + // regression would change the rendered string. + let mut rec = ATR::new(); + rec.mod_tim = 1_000_000_000; + let fields = atdf_types::atdf_data_from_atr(&rec); + assert_eq!(fields[0], "01:46:40 09-Sep-2001"); // MOD_TIM + } + #[cfg(feature = "atdf")] #[test] fn test_atdf_fields_duplicate() { From eb3db41ac31e9c37b932de3bf2992fc122841d16 Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Mon, 8 Jun 2026 13:51:46 +0000 Subject: [PATCH 2/9] fix: replace deprecated chrono from_timestamp_opt Deprecated since chrono 0.4.35 in favor of DateTime::from_timestamp. Output is identical. Bump the chrono floor to 0.4.31, the first release with from_timestamp, so the declared minimum builds. --- Cargo.toml | 2 +- src/atdf_types.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79ee807..bc12039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ smart-default = "0.6.0" flate2 = { version = "1.0.24", optional = true} bzip2 = { version = "0.4.3", optional = true} zip = { version = "0.6.3", default-features = false, features = [ "deflate", "bzip2" ], optional = true } -chrono = { version = "0.4.22", optional = true} +chrono = { version = "0.4.31", optional = true} hex = { version = "0.4.3", optional = true } serde = { version = "1.0.147", features = ["derive"], optional = true} struct-field-names-as-array = { version = "0.1.4", optional = true} diff --git a/src/atdf_types.rs b/src/atdf_types.rs index db237cc..757d2aa 100644 --- a/src/atdf_types.rs +++ b/src/atdf_types.rs @@ -11,7 +11,7 @@ use self::atdf_record_field::*; use crate::{stdf_error::StdfError, stdf_record_type::*, *}; -use chrono::NaiveDateTime; +use chrono::DateTime; use std::collections::hash_map::HashMap; macro_rules! ser_optional { @@ -1104,11 +1104,11 @@ pub(crate) fn atdf_data_from_mir(rec: &MIR) -> Vec { rec.job_nam.clone(), //JOB_NAM rec.node_nam.clone(), //NODE_NAM rec.tstr_typ.clone(), //TSTR_TYP - NaiveDateTime::from_timestamp_opt(rec.setup_t as i64, 0) + DateTime::from_timestamp(rec.setup_t as i64, 0) .expect("invalid or out-of-range datetime") .format("%H:%M:%S %d-%b-%Y") .to_string(), //SETUP_T - NaiveDateTime::from_timestamp_opt(rec.start_t as i64, 0) + DateTime::from_timestamp(rec.start_t as i64, 0) .expect("invalid or out-of-range datetime") .format("%H:%M:%S %d-%b-%Y") .to_string(), //START_T @@ -1149,7 +1149,7 @@ pub(crate) fn atdf_data_from_mir(rec: &MIR) -> Vec { #[inline(always)] pub(crate) fn atdf_data_from_mrr(rec: &MRR) -> Vec { vec![ - NaiveDateTime::from_timestamp_opt(rec.finish_t as i64, 0) + DateTime::from_timestamp(rec.finish_t as i64, 0) .expect("invalid or out-of-range datetime") .format("%H:%M:%S %d-%b-%Y") .to_string(), //FINISH_T @@ -1373,7 +1373,7 @@ pub(crate) fn atdf_data_from_far(rec: &FAR) -> Vec { #[inline(always)] pub(crate) fn atdf_data_from_atr(rec: &ATR) -> Vec { vec![ - NaiveDateTime::from_timestamp_opt(rec.mod_tim as i64, 0) + DateTime::from_timestamp(rec.mod_tim as i64, 0) .expect("invalid or out-of-range datetime") .format("%H:%M:%S %d-%b-%Y") .to_string(), // MOD_TIM From ce745e9e322c40f96f9a89772c080db8e34bf674 Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Tue, 9 Jun 2026 03:53:45 +0000 Subject: [PATCH 3/9] refactor: drop dummy element from empty reads vec![ctor; 0] built an element only to discard it. Build the empty vec directly and drop the now-unused macro default argument. --- src/lib.rs | 4 ++-- src/stdf_types.rs | 42 +++++++++++++++++------------------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 462c853..7a2c165 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -647,7 +647,7 @@ mod tests { ); assert_eq!(pos, 12); assert_eq!( - vec!["".to_string(); 0], + Vec::::new(), stdf_types::read_kx_cn(&raw_data, &mut pos, 0) ); } @@ -679,7 +679,7 @@ mod tests { ); assert_eq!(pos, 15); assert_eq!( - vec!["".to_string(); 0], + Vec::::new(), stdf_types::read_kx_sn(&raw_data, &mut pos, &order, 0) ); } diff --git a/src/stdf_types.rs b/src/stdf_types.rs index 083eb1c..0be116e 100644 --- a/src/stdf_types.rs +++ b/src/stdf_types.rs @@ -2533,17 +2533,13 @@ macro_rules! read_multi_byte_num { } macro_rules! read_multi_element { - ($count:expr, $default:expr, $func:ident($($arg:tt)+)) => { + ($count:expr, $func:ident($($arg:tt)+)) => { { - if $count != 0 { - let mut value = Vec::with_capacity($count as usize); - for _ in 0..$count { - value.push( $func($($arg)+) ); - } - value - } else { - vec![$default; 0] + let mut value = Vec::with_capacity($count as usize); + for _ in 0..$count { + value.push( $func($($arg)+) ); } + value } } } @@ -2695,51 +2691,47 @@ pub(crate) fn read_dn(raw_data: &[u8], pos: &mut usize, order: &ByteOrder) -> Dn /// Read KxCn (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_kx_cn(raw_data: &[u8], pos: &mut usize, k: u16) -> KxCn { - read_multi_element!(k, String::new(), read_cn(raw_data, pos)) + read_multi_element!(k, read_cn(raw_data, pos)) } /// Read KxSn (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_kx_sn(raw_data: &[u8], pos: &mut usize, order: &ByteOrder, k: u16) -> KxSn { - read_multi_element!(k, String::new(), read_sn(raw_data, pos, order)) + read_multi_element!(k, read_sn(raw_data, pos, order)) } /// Read KxCf (Vec) from byte array with offset "pos", vector size is provide by "k", String size is "f" #[inline(always)] pub(crate) fn read_kx_cf(raw_data: &[u8], pos: &mut usize, k: u16, f: u8) -> KxCf { - if k != 0 { - let mut value = Vec::with_capacity(k as usize); - for _ in 0..k { - value.push(read_cf(raw_data, pos, f)); - } - value - } else { - vec!["".to_string(); 0] + let mut value = Vec::with_capacity(k as usize); + for _ in 0..k { + value.push(read_cf(raw_data, pos, f)); } + value } /// Read KxU1 (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_kx_u1(raw_data: &[u8], pos: &mut usize, k: u16) -> KxU1 { - read_multi_element!(k, 0, read_uint8(raw_data, pos)) + read_multi_element!(k, read_uint8(raw_data, pos)) } /// Read KxU2 (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_kx_u2(raw_data: &[u8], pos: &mut usize, order: &ByteOrder, k: u16) -> KxU2 { - read_multi_element!(k, 0, read_u2(raw_data, pos, order)) + read_multi_element!(k, read_u2(raw_data, pos, order)) } /// Read KxU4 (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_kx_u4(raw_data: &[u8], pos: &mut usize, order: &ByteOrder, k: u16) -> KxU4 { - read_multi_element!(k, 0, read_u4(raw_data, pos, order)) + read_multi_element!(k, read_u4(raw_data, pos, order)) } /// Read KxU8 (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_kx_u8(raw_data: &[u8], pos: &mut usize, order: &ByteOrder, k: u16) -> KxU8 { - read_multi_element!(k, 0, read_u8(raw_data, pos, order)) + read_multi_element!(k, read_u8(raw_data, pos, order)) } /// Read KxUf (Vec) from byte array with offset "pos", vector size is provide by "k", size of number is "f" @@ -2767,7 +2759,7 @@ pub(crate) fn read_kx_uf( /// Read KxR4 (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_kx_r4(raw_data: &[u8], pos: &mut usize, order: &ByteOrder, k: u16) -> KxR4 { - read_multi_element!(k, 0.0, read_r4(raw_data, pos, order)) + read_multi_element!(k, read_r4(raw_data, pos, order)) } /// Read KxN1 (Vec) from byte array with offset "pos", vector size is provide by "k" @@ -2821,7 +2813,7 @@ pub(crate) fn read_v1(raw_data: &[u8], pos: &mut usize, order: &ByteOrder) -> V1 /// Read Vn (Vec) from byte array with offset "pos", vector size is provide by "k" #[inline(always)] pub(crate) fn read_vn(raw_data: &[u8], pos: &mut usize, order: &ByteOrder, k: u16) -> Vn { - read_multi_element!(k, V1::Invalid, read_v1(raw_data, pos, order)) + read_multi_element!(k, read_v1(raw_data, pos, order)) } #[inline(always)] From ff8dc66e377d570257fe04ce0d426b93346f6cf3 Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Tue, 9 Jun 2026 03:53:45 +0000 Subject: [PATCH 4/9] refactor: use div_ceil for Dn bytecount bitcount.div_ceil(8) replaces the hand-rolled (bitcount + 7) / 8. It reads clearer and is overflow-safe. Raises the effective MSRV to Rust 1.73, where usize::div_ceil stabilized. --- src/stdf_types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stdf_types.rs b/src/stdf_types.rs index 0be116e..24301fc 100644 --- a/src/stdf_types.rs +++ b/src/stdf_types.rs @@ -2675,7 +2675,7 @@ pub(crate) fn read_bn(raw_data: &[u8], pos: &mut usize) -> Bn { #[inline(always)] pub(crate) fn read_dn(raw_data: &[u8], pos: &mut usize, order: &ByteOrder) -> Dn { let bitcount = read_u2(raw_data, pos, order) as usize; - let bytecount = (bitcount + 7) / 8; + let bytecount = bitcount.div_ceil(8); if bytecount != 0 { let min_pos = std::cmp::min(*pos + bytecount, raw_data.len()); let data_slice = &raw_data[*pos..min_pos]; From 605df747ddf44260b0c3a7373bd87e9e5dc82945 Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Tue, 9 Jun 2026 03:53:45 +0000 Subject: [PATCH 5/9] style: drop redundant usize cast in read_v1 pos is already usize, so the (*pos as usize) cast was a no-op. --- src/stdf_types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stdf_types.rs b/src/stdf_types.rs index 24301fc..8942dae 100644 --- a/src/stdf_types.rs +++ b/src/stdf_types.rs @@ -2786,7 +2786,7 @@ pub(crate) fn read_kx_n1(raw_data: &[u8], pos: &mut usize, k: u16) -> KxN1 { /// Read V1 (u8 + generic value) from byte array with offset "pos" #[inline(always)] pub(crate) fn read_v1(raw_data: &[u8], pos: &mut usize, order: &ByteOrder) -> V1 { - let type_byte = if (*pos as usize) < raw_data.len() { + let type_byte = if *pos < raw_data.len() { read_uint8(raw_data, pos) } else { 0xF From 36a26e4cc66d15940ab514578faf5de57658c8a3 Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Tue, 9 Jun 2026 03:54:14 +0000 Subject: [PATCH 6/9] style: annotate zip transmute, iterator lifetimes Make the transmute's source type and the iterators' elided &mut self lifetimes explicit. Also document why the 'static extension is sound: stable Box address, drop order, and no aliasing while the file is alive. --- src/atdf_file.rs | 2 +- src/stdf_file.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/atdf_file.rs b/src/atdf_file.rs index b7b5f90..91fbcbc 100644 --- a/src/atdf_file.rs +++ b/src/atdf_file.rs @@ -106,7 +106,7 @@ impl AtdfReader { } #[inline(always)] - pub fn get_record_iter(&mut self) -> AtdfRecordIter { + pub fn get_record_iter(&mut self) -> AtdfRecordIter<'_, R> { AtdfRecordIter { inner: self, incomplete_rec: String::new(), diff --git a/src/stdf_file.rs b/src/stdf_file.rs index 4f7c56b..2ca84d8 100644 --- a/src/stdf_file.rs +++ b/src/stdf_file.rs @@ -205,7 +205,7 @@ impl StdfReader { /// Only the records after the current file position /// can be read. #[inline(always)] - pub fn get_record_iter(&mut self) -> RecordIter { + pub fn get_record_iter(&mut self) -> RecordIter<'_, R> { RecordIter { inner: self, buffer: Vec::new(), @@ -217,7 +217,7 @@ impl StdfReader { /// beware that internal `offset` counter is starting /// from the current position. #[inline(always)] - pub fn get_rawdata_iter(&mut self) -> RawDataIter { + pub fn get_rawdata_iter(&mut self) -> RawDataIter<'_, R> { RawDataIter { offset: 0, inner: self, @@ -233,8 +233,13 @@ impl ZipBundle { let archive = ZipArchive::new(stream)?; let mut archive = Box::new(archive); - let file = - unsafe { std::mem::transmute::<_, ZipFile<'static>>(archive.by_index(file_index)?) }; + // SAFETY: fakes a 'static borrow of `archive`. Sound because `archive` is + // boxed (address stable across the move below), `file` drops before + // `archive` (whose Drop reads it), and `archive` is never aliased while + // `file` is alive (reopen_file clears `file` before re-borrowing). + let file = unsafe { + std::mem::transmute::, ZipFile<'static>>(archive.by_index(file_index)?) + }; Ok(ZipBundle { archive, file: Some(file), @@ -242,9 +247,11 @@ impl ZipBundle { } pub(crate) fn reopen_file(&mut self, file_index: usize) -> Result<(), StdfError> { + // Drop the outstanding borrow before re-borrowing `archive`. self.file = None; + // SAFETY: same invariants as `new`. `file` was just cleared above. let file = unsafe { - std::mem::transmute::<_, ZipFile<'static>>(self.archive.by_index(file_index)?) + std::mem::transmute::, ZipFile<'static>>(self.archive.by_index(file_index)?) }; self.file = Some(file); Ok(()) From 0dc65c78eeaae97649c53ea5d543ed29676b79d5 Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Mon, 8 Jun 2026 13:34:32 +0000 Subject: [PATCH 7/9] refactor: drop the no-op scale_flag check The empty if did nothing. The field it read is now write-only, but its value comes from from_atdf_string's frozen public param, so keep it as allow(dead_code) rather than change the signature. --- src/atdf_types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/atdf_types.rs b/src/atdf_types.rs index 757d2aa..2826d4e 100644 --- a/src/atdf_types.rs +++ b/src/atdf_types.rs @@ -332,7 +332,8 @@ pub(crate) mod atdf_record_field { pub struct AtdfRecord { rec_name: String, type_code: u64, - scale_flag: bool, // currently not used... maybe in the future + #[allow(dead_code)] // currently not used... maybe in the future + scale_flag: bool, data_map: HashMap, } @@ -340,7 +341,6 @@ impl From<&AtdfRecord> for StdfRecord { #[inline(always)] fn from(atdf_rec: &AtdfRecord) -> Self { //TODO - if atdf_rec.scale_flag {} StdfRecord::new(atdf_rec.type_code) } } From da6df04c2b5b4c95dff98718e33ed8253356c543 Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Mon, 8 Jun 2026 13:34:32 +0000 Subject: [PATCH 8/9] style: drop stale doc comment and redundant import The doc comment sat on commented-out code. The bare `use serde_json;` is redundant under edition 2021, since the crate is already in scope via fully-qualified paths. Neither is load-bearing. --- example/stdf_to_xlsx.rs | 1 - src/atdf_types.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/example/stdf_to_xlsx.rs b/example/stdf_to_xlsx.rs index e9e6143..4157a93 100644 --- a/example/stdf_to_xlsx.rs +++ b/example/stdf_to_xlsx.rs @@ -17,7 +17,6 @@ use rust_stdf::{stdf_file::*, stdf_record_type::*, StdfRecord}; use rust_xlsxwriter::{Workbook, Worksheet, XlsxError}; -use serde_json; use std::collections::HashMap; use std::env; diff --git a/src/atdf_types.rs b/src/atdf_types.rs index 2826d4e..26cb7af 100644 --- a/src/atdf_types.rs +++ b/src/atdf_types.rs @@ -935,7 +935,7 @@ pub(crate) fn atdf_data_from_ftr(rec: &FTR) -> Vec { ] } -/// ignored because I do not know ATDF structure in V4-2007 +// ignored because I do not know ATDF structure in V4-2007 // pub(crate) fn atdf_data_from_str_rec(rec: &STR) -> Vec { // vec![]} From 1dc35994338dd6202515e004158d4bfd0e4b46ab Mon Sep 17 00:00:00 2001 From: Henrik Nilsson Date: Tue, 9 Jun 2026 03:25:06 +0000 Subject: [PATCH 9/9] style: adopt clippy idioms in tests Clear the remaining clippy lints in test code (single_match, ptr_arg, clone_on_copy, expect_fun_call). Behavior is preserved, except a file-open failure now reports the underlying error. --- tests/record_serialize_test.rs | 26 ++++++++++---------------- tests/stdf_file_tests.rs | 12 ++++++------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/tests/record_serialize_test.rs b/tests/record_serialize_test.rs index 623c6f3..82660d6 100644 --- a/tests/record_serialize_test.rs +++ b/tests/record_serialize_test.rs @@ -18,13 +18,10 @@ use serde_json::{self, json}; #[cfg(feature = "serialize")] fn record_ser_test() { // check upper case - match StdfRecord::new(REC_FAR) { - StdfRecord::FAR(r) => { - let json = serde_json::to_value(&r).unwrap(); - assert_eq!(json["CPU_TYPE"], json!(0)); - assert_eq!(json["cpu_type"], serde_json::Value::Null); - } - _ => {} + if let StdfRecord::FAR(r) = StdfRecord::new(REC_FAR) { + let json = serde_json::to_value(&r).unwrap(); + assert_eq!(json["CPU_TYPE"], json!(0)); + assert_eq!(json["cpu_type"], serde_json::Value::Null); } // check GDR @@ -36,15 +33,12 @@ fn record_ser_test() { V1::N1(8), ], }); - match gdr_rec { - StdfRecord::GDR(r) => { - let json = serde_json::to_value(&r).unwrap(); - assert_eq!(json["FLD_CNT"], json!(3)); - assert_eq!(json["GEN_DATA"][0]["Cn"], json!("test")); - assert_eq!(json["GEN_DATA"][1]["Bn"], json!(vec![1, 2, 3, 4, 5, 6, 7])); - assert_eq!(json["GEN_DATA"][2]["N1"], json!(8)); - } - _ => {} + if let StdfRecord::GDR(r) = gdr_rec { + let json = serde_json::to_value(&r).unwrap(); + assert_eq!(json["FLD_CNT"], json!(3)); + assert_eq!(json["GEN_DATA"][0]["Cn"], json!("test")); + assert_eq!(json["GEN_DATA"][1]["Bn"], json!(vec![1, 2, 3, 4, 5, 6, 7])); + assert_eq!(json["GEN_DATA"][2]["N1"], json!(8)); } // check fields names diff --git a/tests/stdf_file_tests.rs b/tests/stdf_file_tests.rs index f52571a..aafaece 100644 --- a/tests/stdf_file_tests.rs +++ b/tests/stdf_file_tests.rs @@ -14,14 +14,14 @@ use rust_stdf::{stdf_file::*, stdf_record_type::*, StdfRecord}; use std::{ fs::{self, read_dir}, io::{Read, Seek, SeekFrom}, - path::PathBuf, + path::{Path, PathBuf}, }; fn get_test_stdf_files() -> Vec { let mut test_folder = PathBuf::from(env!("CARGO_MANIFEST_DIR")); test_folder.push("demo_stdf"); - fn supported_ext(p: &PathBuf) -> bool { + fn supported_ext(p: &Path) -> bool { let p = p.display().to_string(); let file_ext = p.rsplit('.').next(); match file_ext { @@ -43,7 +43,7 @@ fn get_test_stdf_files() -> Vec { read_dir(test_folder) .unwrap() .map(|ent| ent.unwrap().path().to_path_buf()) - .filter(supported_ext) + .filter(|p| supported_ext(p)) .collect::>() } @@ -53,8 +53,8 @@ fn supported_stdf_file_test() { assert_ne!(stdf_file_list.len(), 0); for file in stdf_file_list.iter() { - let mut reader = - StdfReader::new(file).expect(&format!("error when open {}", file.display())); + let mut reader = StdfReader::new(file) + .unwrap_or_else(|e| panic!("error when open {}: {e}", file.display())); let mut record_positions_list = Vec::with_capacity(2048); @@ -69,7 +69,7 @@ fn supported_stdf_file_test() { raw_rec.header.get_type(), raw_rec.offset, raw_rec.raw_data.len(), - raw_rec.byte_order.clone(), + raw_rec.byte_order, )); if count != 0 {