falcon_mdf 0.7.1

High-performance Rust library for reading ASAM MDF v4 (MF4) measurement data files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Low-level binary reading utilities.
//!
//! This module provides efficient utilities for reading binary data
//! with explicit endianness handling.

use byteorder::{BigEndian, ByteOrder, LittleEndian};

/// Reads an unsigned integer of 1-8 bytes from a byte slice.
///
/// # Arguments
/// * `data` - The byte slice to read from
/// * `byte_offset` - The byte offset within the first byte
/// * `bit_offset` - The bit offset within the first byte
/// * `bit_count` - The total number of bits to read
/// * `little_endian` - Whether to use little-endian byte order
///
/// # Returns
/// The value as a u64, or 0 if the parameters are invalid.
pub fn read_uint(
    data: &[u8],
    byte_offset: usize,
    bit_offset: u8,
    bit_count: u32,
    little_endian: bool,
) -> u64 {
    if bit_count == 0 || bit_count > 64 {
        return 0;
    }

    // MDF 4.x starts a field inside the first byte it touches: `cn_bit_offset`
    // is 0..=7. A larger value from a malformed file is not a field position
    // but a corrupt declaration — it widens the read window and eventually
    // shifts a u128 by `bit_offset` bits, which is an overflow panic in debug
    // builds and a silently wrapped number in release. Invalid parameters read
    // as zero, so refuse it here rather than produce either.
    if bit_offset > 7 {
        return 0;
    }

    let byte_count = (bit_offset as u32 + bit_count).div_ceil(8) as usize;
    let end_offset = match byte_offset.checked_add(byte_count) {
        Some(end) => end,
        None => return 0,
    };
    if end_offset > data.len() {
        return 0;
    }

    // Handle aligned byte reads (common case)
    if bit_offset == 0 && bit_count.is_multiple_of(8) {
        let bytes = &data[byte_offset..end_offset];
        return match byte_count {
            1 => bytes[0] as u64,
            2 => {
                if little_endian {
                    LittleEndian::read_u16(bytes) as u64
                } else {
                    BigEndian::read_u16(bytes) as u64
                }
            }
            3 | 4 => {
                let mut buf = [0u8; 4];
                buf[..byte_count].copy_from_slice(bytes);
                if little_endian {
                    LittleEndian::read_u32(&buf) as u64
                } else {
                    // For big-endian, right-align
                    buf.rotate_right(4 - byte_count);
                    BigEndian::read_u32(&buf) as u64
                }
            }
            5..=8 => {
                let mut buf = [0u8; 8];
                buf[..byte_count].copy_from_slice(bytes);
                if little_endian {
                    LittleEndian::read_u64(&buf)
                } else {
                    buf.rotate_right(8 - byte_count);
                    BigEndian::read_u64(&buf)
                }
            }
            _ => 0,
        };
    }

    // Unaligned bit reads.
    //
    // A 64-bit field starting part-way into a byte spans nine bytes, so the
    // window does not fit in a u64 while it is being assembled. Accumulate in a
    // u128 and narrow only after shifting the field down to bit zero; doing this
    // in u64 overflows the shift and panics.
    let bytes = &data[byte_offset..end_offset];
    let mut value: u128 = 0;

    if little_endian {
        // Little-endian: first byte is LSB
        for (i, &byte) in bytes.iter().enumerate() {
            value |= (byte as u128) << (i * 8);
        }
    } else {
        // Big-endian: first byte is MSB
        for &byte in bytes {
            value = (value << 8) | (byte as u128);
        }
    }

    value >>= bit_offset;

    // `1 << 64` is itself an overflow, so a full-width field masks to all ones.
    let mask: u128 = if bit_count >= 64 {
        u64::MAX as u128
    } else {
        (1u128 << bit_count) - 1
    };
    (value & mask) as u64
}

#[doc(hidden)]
pub use self::read_uint as read_bits;

/// Reads a signed integer, sign-extending from the specified bit count.
pub fn read_int(
    data: &[u8],
    byte_offset: usize,
    bit_offset: u8,
    bit_count: u32,
    little_endian: bool,
) -> i64 {
    let unsigned = read_uint(data, byte_offset, bit_offset, bit_count, little_endian);

    // Sign extend
    if bit_count > 0 && bit_count < 64 {
        let sign_bit = 1u64 << (bit_count - 1);
        if unsigned & sign_bit != 0 {
            // Negative number, sign extend
            let mask = !((1u64 << bit_count) - 1);
            return (unsigned | mask) as i64;
        }
    }

    unsigned as i64
}

/// Reads an f32 from a byte slice.
pub fn read_f32(data: &[u8], offset: usize, little_endian: bool) -> f32 {
    let end = match offset.checked_add(4) {
        Some(end) => end,
        None => return 0.0,
    };
    if end > data.len() {
        return 0.0;
    }
    let bytes = &data[offset..end];
    if little_endian {
        LittleEndian::read_f32(bytes)
    } else {
        BigEndian::read_f32(bytes)
    }
}

/// Reads an f64 from a byte slice.
pub fn read_f64(data: &[u8], offset: usize, little_endian: bool) -> f64 {
    let end = match offset.checked_add(8) {
        Some(end) => end,
        None => return 0.0,
    };
    if end > data.len() {
        return 0.0;
    }
    let bytes = &data[offset..end];
    if little_endian {
        LittleEndian::read_f64(bytes)
    } else {
        BigEndian::read_f64(bytes)
    }
}

/// Converts raw bytes to f64 based on data type and bit count.
pub fn bytes_to_f64(
    data: &[u8],
    byte_offset: usize,
    bit_offset: u8,
    bit_count: u32,
    is_signed: bool,
    is_float: bool,
    little_endian: bool,
) -> f64 {
    if is_float {
        match bit_count {
            32 => read_f32(data, byte_offset, little_endian) as f64,
            64 => read_f64(data, byte_offset, little_endian),
            _ => 0.0,
        }
    } else if is_signed {
        read_int(data, byte_offset, bit_offset, bit_count, little_endian) as f64
    } else {
        read_uint(data, byte_offset, bit_offset, bit_count, little_endian) as f64
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_read_uint_le() {
        let data = [0x01, 0x02, 0x03, 0x04];
        assert_eq!(read_uint(&data, 0, 0, 8, true), 0x01);
        assert_eq!(read_uint(&data, 0, 0, 16, true), 0x0201);
        assert_eq!(read_uint(&data, 0, 0, 32, true), 0x04030201);
    }

    #[test]
    fn test_read_uint_be() {
        let data = [0x01, 0x02, 0x03, 0x04];
        assert_eq!(read_uint(&data, 0, 0, 8, false), 0x01);
        assert_eq!(read_uint(&data, 0, 0, 16, false), 0x0102);
        assert_eq!(read_uint(&data, 0, 0, 32, false), 0x01020304);
    }

    #[test]
    fn test_read_int_signed() {
        // -1 as 8-bit signed
        let data = [0xFF];
        assert_eq!(read_int(&data, 0, 0, 8, true), -1);

        // -1 as 16-bit signed
        let data = [0xFF, 0xFF];
        assert_eq!(read_int(&data, 0, 0, 16, true), -1);

        // Positive value
        let data = [0x7F, 0x00];
        assert_eq!(read_int(&data, 0, 0, 16, true), 127);
    }

    #[test]
    fn test_read_f32() {
        // 1.0 as f32 in little-endian
        let data = [0x00, 0x00, 0x80, 0x3F];
        assert!((read_f32(&data, 0, true) - 1.0).abs() < 0.0001);
    }

    #[test]
    fn test_read_f64() {
        // 1.0 as f64 in little-endian
        let data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F];
        assert!((read_f64(&data, 0, true) - 1.0).abs() < 0.0001);
    }

    #[test]
    fn a_bit_offset_past_the_first_byte_reads_as_zero() {
        // MDF 4.x allows a field to start only inside the first byte it
        // touches (`cn_bit_offset` is 0..=7). A hostile value such as 8, 64 or
        // 255 previously shifted a u128 by that many bits — bit_offset 255
        // spans 33 bytes and shifts the little-endian assembly by up to 256
        // bits — an overflow panic in debug builds, and a silently wrapped
        // number in release. Invalid parameters read as 0, so every entry point
        // that funnels through `read_uint` must return zero rather than a wrong
        // value or a crash. The 64-byte buffer is big enough that the guard is
        // what rejects the 255-bit case, not the bounds check.
        let data = [0xFFu8; 64];
        for little_endian in [true, false] {
            for &bit_offset in &[8u8, 64u8, 255u8] {
                let label = format!("bit_offset {bit_offset}, little_endian {little_endian}");
                assert_eq!(
                    read_uint(&data, 0, bit_offset, 8, little_endian),
                    0,
                    "{label}"
                );
                assert_eq!(
                    read_int(&data, 0, bit_offset, 8, little_endian),
                    0,
                    "{label}"
                );
                assert_eq!(
                    bytes_to_f64(&data, 0, bit_offset, 8, false, false, little_endian),
                    0.0,
                    "{label}"
                );
            }
        }
    }

    #[test]
    fn test_overflow_offsets() {
        let data = [0xFFu8; 64];
        for little_endian in [true, false] {
            assert_eq!(read_uint(&data, usize::MAX, 0, 8, little_endian), 0);
            assert_eq!(read_uint(&data, usize::MAX - 4, 0, 64, little_endian), 0);
            assert_eq!(read_uint(&data, usize::MAX - 8, 4, 64, little_endian), 0);
            assert_eq!(read_int(&data, usize::MAX, 0, 8, little_endian), 0);
            assert_eq!(read_f32(&data, usize::MAX, little_endian), 0.0);
            assert_eq!(read_f64(&data, usize::MAX, little_endian), 0.0);
        }
    }

    #[test]
    fn test_bytes_to_f64() {
        // Float
        let data = [0x00, 0x00, 0x80, 0x3F];
        assert!((bytes_to_f64(&data, 0, 0, 32, false, true, true) - 1.0).abs() < 0.0001);

        // Unsigned int
        let data = [0x64, 0x00]; // 100 in LE u16
        assert!((bytes_to_f64(&data, 0, 0, 16, false, false, true) - 100.0).abs() < 0.0001);

        // Signed int
        let data = [0xFF, 0xFF]; // -1 in LE i16
        assert!((bytes_to_f64(&data, 0, 0, 16, true, false, true) - (-1.0)).abs() < 0.0001);
    }
}

#[cfg(test)]
mod mask_tests {
    use super::{read_int, read_uint};

    #[test]
    fn reads_a_full_width_field_that_is_not_byte_aligned() {
        // A 64-bit field starting at bit 4. Building the mask as
        // `(1 << bit_count) - 1` overflows for bit_count == 64, which panics in
        // debug builds; a malformed file can declare exactly this layout.
        let data = [0xFFu8; 16];
        let v = read_uint(&data, 0, 4, 64, true);
        assert_eq!(v, u64::MAX, "all bits set should read back as all bits set");
    }

    #[test]
    fn reads_a_63_bit_unaligned_field() {
        let data = [0xFFu8; 16];
        let v = read_uint(&data, 0, 1, 63, true);
        assert_eq!(v, (1u64 << 63) - 1);
    }

    #[test]
    fn sign_extends_a_full_width_unaligned_field() {
        let data = [0xFFu8; 16];
        assert_eq!(read_int(&data, 0, 4, 64, true), -1);
    }
}

#[cfg(test)]
mod big_endian_tests {
    use super::{read_int, read_uint};

    // Expected values here are derived from the semantics the reference
    // implementation uses: assemble the field's bytes most-significant first,
    // shift right by the bit offset, then mask to the bit count. Its handling of
    // fields narrower than a standard width — pad with trailing zero bytes, then
    // shift by `extra_bytes * 8 + bit_offset` — is equivalent to assembling only
    // the real bytes, which is what this code does.

    #[test]
    fn reads_whole_byte_fields_most_significant_first() {
        let data = [0x12, 0x34, 0x56, 0x78];
        assert_eq!(read_uint(&data, 0, 0, 8, false), 0x12);
        assert_eq!(read_uint(&data, 0, 0, 16, false), 0x1234);
        assert_eq!(read_uint(&data, 0, 0, 24, false), 0x12_3456);
        assert_eq!(read_uint(&data, 0, 0, 32, false), 0x1234_5678);
    }

    #[test]
    fn reads_a_full_width_big_endian_field() {
        let data = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
        assert_eq!(read_uint(&data, 0, 0, 64, false), 0x0123_4567_89AB_CDEF);
    }

    #[test]
    fn big_and_little_endian_disagree_as_expected() {
        let data = [0xAA, 0xBB];
        assert_eq!(read_uint(&data, 0, 0, 16, false), 0xAABB);
        assert_eq!(read_uint(&data, 0, 0, 16, true), 0xBBAA);
    }

    #[test]
    fn reads_from_a_byte_offset() {
        let data = [0x00, 0x00, 0x12, 0x34];
        assert_eq!(read_uint(&data, 2, 0, 16, false), 0x1234);
    }

    #[test]
    fn a_bit_offset_shifts_the_assembled_field_down() {
        // 0xFF00 assembled big-endian, shifted right 4, masked to 12 bits.
        let data = [0xFF, 0x00];
        assert_eq!(read_uint(&data, 0, 0, 12, false), 0xF00);
        assert_eq!(read_uint(&data, 0, 4, 12, false), 0xFF0);
    }

    #[test]
    fn reads_sub_byte_fields() {
        // 0b1010_1100 as the only byte.
        let data = [0b1010_1100];
        assert_eq!(read_uint(&data, 0, 0, 4, false), 0b1100);
        assert_eq!(read_uint(&data, 0, 2, 4, false), 0b1011);
        assert_eq!(read_uint(&data, 0, 4, 4, false), 0b1010);
        assert_eq!(read_uint(&data, 0, 7, 1, false), 1);
    }

    #[test]
    fn sign_extends_from_the_field_width() {
        assert_eq!(read_int(&[0xFF, 0xFF], 0, 0, 16, false), -1);
        assert_eq!(read_int(&[0x80, 0x00], 0, 0, 16, false), i16::MIN as i64);
        assert_eq!(read_int(&[0x7F, 0xFF], 0, 0, 16, false), i16::MAX as i64);
        // A 12-bit field whose top bit is set.
        assert_eq!(read_int(&[0x0F, 0xFF], 0, 0, 12, false), -1);
    }

    #[test]
    fn a_field_running_past_the_buffer_reads_as_zero_rather_than_panicking() {
        let data = [0x12];
        assert_eq!(read_uint(&data, 0, 0, 32, false), 0);
        assert_eq!(read_uint(&data, 4, 0, 8, false), 0);
    }

    #[test]
    fn the_aligned_and_general_paths_agree_for_big_endian() {
        // `read_uint` takes a shortcut for byte-aligned whole-byte fields. That
        // shortcut and the general bit-extraction path must produce the same
        // answer, or which one runs would change the result.
        let data = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67];
        for width in [8u32, 16, 24, 32, 40, 48, 56, 64] {
            let aligned = read_uint(&data, 0, 0, width, false);

            // Recompute independently: the field's bytes, most significant
            // first, masked to its width.
            let bytes = (width / 8) as usize;
            let expected = data[..bytes]
                .iter()
                .fold(0u64, |acc, &b| (acc << 8) | b as u64);
            assert_eq!(aligned, expected, "big-endian {width}-bit field");
        }
    }
}