1use crate::data::{self, format_guid, Value};
4use crate::encoding;
5use crate::file::{FileError, PageReader};
6use crate::format::CATALOG_PAGE;
7use crate::money;
8use crate::table;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PropMapType {
17 Default,
19 Column,
21 Additional,
23}
24
25#[derive(Debug, Clone)]
27pub struct Property {
28 pub name: String,
29 pub value: Value,
30 pub ddl: bool,
31}
32
33#[derive(Debug, Clone)]
35pub struct PropertyMap {
36 pub map_type: PropMapType,
37 pub name: String,
39 pub properties: Vec<Property>,
40}
41
42#[derive(Debug, Clone)]
44pub struct ObjectProperties {
45 pub object_name: String,
46 pub maps: Vec<PropertyMap>,
47}
48
49pub fn read_object_properties(
55 reader: &mut PageReader,
56 object_name: &str,
57) -> Result<ObjectProperties, FileError> {
58 let is_jet3 = reader.header().version.is_jet3();
59 let tdef = table::read_table_def(reader, "MSysObjects", CATALOG_PAGE)?;
60 let result = data::read_table_rows(reader, &tdef)?;
61 result.warn_skipped("MSysObjects");
62
63 let (mut name_idx, mut lvprop_idx) = (None, None);
65 for (i, col) in tdef.columns.iter().enumerate() {
66 match col.name.as_str() {
67 "Name" => name_idx = Some(i),
68 "LvProp" => lvprop_idx = Some(i),
69 _ => {}
70 }
71 }
72 let name_idx = name_idx.ok_or(FileError::InvalidTableDef {
73 reason: "MSysObjects missing Name column",
74 })?;
75
76 let lvprop_idx = match lvprop_idx {
77 Some(i) => i,
78 None => {
79 return Ok(ObjectProperties {
80 object_name: object_name.to_string(),
81 maps: Vec::new(),
82 });
83 }
84 };
85
86 for row in &result.rows {
88 let row_name = match row.get(name_idx) {
89 Some(Value::Text(s)) => s.as_str(),
90 _ => continue,
91 };
92 if row_name != object_name {
93 continue;
94 }
95
96 let data = match row.get(lvprop_idx) {
97 Some(Value::Binary(b)) => b,
98 _ => {
99 return Ok(ObjectProperties {
100 object_name: object_name.to_string(),
101 maps: Vec::new(),
102 });
103 }
104 };
105
106 let maps = parse_lvprop(data, is_jet3)?;
107 return Ok(ObjectProperties {
108 object_name: object_name.to_string(),
109 maps,
110 });
111 }
112
113 Ok(ObjectProperties {
115 object_name: object_name.to_string(),
116 maps: Vec::new(),
117 })
118}
119
120const HEADER_JET3: &[u8; 4] = b"KKD\0";
126const HEADER_JET4: &[u8; 4] = b"MR2\0";
128
129const CHUNK_NAME_LIST: u16 = 0x0080;
131
132fn validate_header(data: &[u8]) -> Result<(), FileError> {
134 if data.len() < 4 {
135 return Err(FileError::InvalidProperty {
136 reason: "LvProp data too short for header",
137 });
138 }
139 if &data[..4] == HEADER_JET3 || &data[..4] == HEADER_JET4 {
140 Ok(())
141 } else {
142 Err(FileError::InvalidProperty {
143 reason: "LvProp header: unknown magic bytes",
144 })
145 }
146}
147
148pub(crate) fn parse_lvprop(data: &[u8], is_jet3: bool) -> Result<Vec<PropertyMap>, FileError> {
150 validate_header(data)?;
151
152 let mut offset = 4; let mut names: Vec<String> = Vec::new();
154 let mut maps: Vec<PropertyMap> = Vec::new();
155
156 while offset + 6 <= data.len() {
157 let chunk_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
158 let chunk_type = u16::from_le_bytes(data[offset + 4..offset + 6].try_into().unwrap());
159
160 if chunk_len < 6 {
161 return Err(FileError::InvalidProperty {
162 reason: "LvProp chunk_len < 6",
163 });
164 }
165 if offset + chunk_len > data.len() {
166 break;
168 }
169
170 let chunk_data = &data[offset + 6..offset + chunk_len];
171
172 match chunk_type {
173 CHUNK_NAME_LIST => {
174 names = parse_name_list(chunk_data, is_jet3)?;
175 }
176 0x0000..=0x0002 => {
177 let map = parse_value_chunk(chunk_data, chunk_type, &names, is_jet3)?;
178 maps.push(map);
179 }
180 _ => {
181 }
183 }
184
185 offset += chunk_len;
186 }
187
188 Ok(maps)
189}
190
191fn parse_name_list(chunk_data: &[u8], is_jet3: bool) -> Result<Vec<String>, FileError> {
193 let mut names = Vec::new();
194 let mut pos = 0;
195
196 while pos + 2 <= chunk_data.len() {
197 let name_len = u16::from_le_bytes(chunk_data[pos..pos + 2].try_into().unwrap()) as usize;
198 pos += 2;
199 if pos + name_len > chunk_data.len() {
200 break;
201 }
202 let name_bytes = &chunk_data[pos..pos + name_len];
203 let name = if is_jet3 {
204 encoding::decode_latin1(name_bytes)
205 } else {
206 encoding::decode_utf16le(name_bytes).map_err(FileError::Format)?
207 };
208 names.push(name);
209 pos += name_len;
210 }
211
212 Ok(names)
213}
214
215fn parse_value_chunk(
217 chunk_data: &[u8],
218 chunk_type: u16,
219 names: &[String],
220 is_jet3: bool,
221) -> Result<PropertyMap, FileError> {
222 let map_type = match chunk_type {
223 0x0000 => PropMapType::Default,
224 0x0001 => PropMapType::Column,
225 0x0002 => PropMapType::Additional,
226 _ => PropMapType::Default,
227 };
228
229 if chunk_data.len() < 4 {
231 return Ok(PropertyMap {
232 map_type,
233 name: String::new(),
234 properties: Vec::new(),
235 });
236 }
237
238 let name_block_len = u32::from_le_bytes(chunk_data[..4].try_into().unwrap()) as usize;
239
240 let block_name = if name_block_len > 6 && chunk_data.len() >= 6 {
242 let block_name_len = u16::from_le_bytes(chunk_data[4..6].try_into().unwrap()) as usize;
243 let name_end = (6 + block_name_len).min(chunk_data.len());
244 let name_bytes = &chunk_data[6..name_end];
245 if is_jet3 {
246 encoding::decode_latin1(name_bytes)
247 } else {
248 encoding::decode_utf16le(name_bytes).unwrap_or_default()
249 }
250 } else {
251 String::new()
252 };
253
254 let entries_start = name_block_len.min(chunk_data.len());
256 let mut properties = Vec::new();
257 let mut pos = entries_start;
258
259 while pos + 8 <= chunk_data.len() {
260 let val_len = u16::from_le_bytes(chunk_data[pos..pos + 2].try_into().unwrap()) as usize;
261 if val_len < 8 {
262 break;
263 }
264
265 let ddl_flag = chunk_data[pos + 2];
266 let data_type = chunk_data[pos + 3];
267 let name_idx =
268 u16::from_le_bytes(chunk_data[pos + 4..pos + 6].try_into().unwrap()) as usize;
269 let data_size =
270 u16::from_le_bytes(chunk_data[pos + 6..pos + 8].try_into().unwrap()) as usize;
271
272 let prop_name = names.get(name_idx).cloned().unwrap_or_default();
273
274 let value_end = (pos + 8 + data_size).min(chunk_data.len());
275 let raw = &chunk_data[pos + 8..value_end];
276
277 let value = decode_prop_value(data_type, raw, &prop_name, is_jet3);
278
279 properties.push(Property {
280 name: prop_name,
281 value,
282 ddl: ddl_flag != 0,
283 });
284
285 pos += val_len;
287 }
288
289 Ok(PropertyMap {
290 map_type,
291 name: block_name,
292 properties,
293 })
294}
295
296fn decode_prop_value(data_type: u8, raw: &[u8], prop_name: &str, is_jet3: bool) -> Value {
298 match data_type {
299 0x01 => {
301 if raw.is_empty() {
302 Value::Bool(false)
303 } else {
304 Value::Bool(raw[0] != 0)
305 }
306 }
307 0x02 => {
309 if raw.is_empty() {
310 Value::Null
311 } else {
312 Value::Byte(raw[0])
313 }
314 }
315 0x03 => {
317 if raw.len() < 2 {
318 Value::Null
319 } else {
320 Value::Int(i16::from_le_bytes([raw[0], raw[1]]))
321 }
322 }
323 0x04 => {
325 if raw.len() < 4 {
326 Value::Null
327 } else {
328 Value::Long(i32::from_le_bytes(raw[..4].try_into().unwrap()))
329 }
330 }
331 0x05 => {
333 if raw.len() < 8 {
334 Value::Null
335 } else {
336 let bytes: [u8; 8] = raw[..8].try_into().unwrap();
337 Value::Money(money::money_to_string(&bytes))
338 }
339 }
340 0x06 => {
342 if raw.len() < 4 {
343 Value::Null
344 } else {
345 Value::Float(f32::from_le_bytes(raw[..4].try_into().unwrap()))
346 }
347 }
348 0x07 => {
350 if raw.len() < 8 {
351 Value::Null
352 } else {
353 Value::Double(f64::from_le_bytes(raw[..8].try_into().unwrap()))
354 }
355 }
356 0x08 => {
358 if raw.len() < 8 {
359 Value::Null
360 } else {
361 Value::Timestamp(f64::from_le_bytes(raw[..8].try_into().unwrap()))
362 }
363 }
364 0x09 => {
366 if raw.len() == 16 && prop_name == "GUID" {
367 Value::Guid(format_guid(raw))
368 } else {
369 Value::Binary(raw.to_vec())
370 }
371 }
372 0x0A => {
374 if is_jet3 {
375 Value::Text(encoding::decode_latin1(raw))
376 } else {
377 match encoding::decode_utf16le(raw) {
378 Ok(s) => Value::Text(s),
379 Err(_) => Value::Binary(raw.to_vec()),
380 }
381 }
382 }
383 0x0B => Value::Binary(raw.to_vec()),
385 0x0C => {
387 if is_jet3 {
388 Value::Text(encoding::decode_latin1(raw))
389 } else {
390 match encoding::decode_utf16le(raw) {
391 Ok(s) => Value::Text(s),
392 Err(_) => Value::Binary(raw.to_vec()),
393 }
394 }
395 }
396 0x0F => {
398 if raw.len() == 16 {
399 Value::Guid(format_guid(raw))
400 } else {
401 Value::Binary(raw.to_vec())
402 }
403 }
404 _ => Value::Binary(raw.to_vec()),
406 }
407}
408
409#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
420 fn validate_header_jet3() {
421 let data = b"KKD\0some extra data";
422 assert!(validate_header(data).is_ok());
423 }
424
425 #[test]
426 fn validate_header_jet4() {
427 let data = b"MR2\0some extra data";
428 assert!(validate_header(data).is_ok());
429 }
430
431 #[test]
432 fn validate_header_invalid() {
433 let data = b"XXXX";
434 assert!(validate_header(data).is_err());
435 }
436
437 #[test]
438 fn validate_header_too_short() {
439 let data = b"MR";
440 assert!(validate_header(data).is_err());
441 }
442
443 #[test]
446 fn parse_name_list_jet3() {
447 let mut data = Vec::new();
449 data.extend_from_slice(&3u16.to_le_bytes());
450 data.extend_from_slice(b"Foo");
451 data.extend_from_slice(&3u16.to_le_bytes());
452 data.extend_from_slice(b"Bar");
453
454 let names = parse_name_list(&data, true).unwrap();
455 assert_eq!(names, vec!["Foo", "Bar"]);
456 }
457
458 #[test]
459 fn parse_name_list_jet4() {
460 let mut data = Vec::new();
462 data.extend_from_slice(&4u16.to_le_bytes());
464 data.extend_from_slice(&[0x48, 0x00, 0x69, 0x00]);
465 data.extend_from_slice(&4u16.to_le_bytes());
467 data.extend_from_slice(&[0x47, 0x00, 0x6F, 0x00]);
468
469 let names = parse_name_list(&data, false).unwrap();
470 assert_eq!(names, vec!["Hi", "Go"]);
471 }
472
473 #[test]
474 fn parse_name_list_empty() {
475 let names = parse_name_list(&[], true).unwrap();
476 assert!(names.is_empty());
477 }
478
479 #[test]
482 fn decode_prop_value_bool_true() {
483 let val = decode_prop_value(0x01, &[0x01], "x", false);
484 assert_eq!(val, Value::Bool(true));
485 }
486
487 #[test]
488 fn decode_prop_value_bool_false() {
489 let val = decode_prop_value(0x01, &[0x00], "x", false);
490 assert_eq!(val, Value::Bool(false));
491 }
492
493 #[test]
494 fn decode_prop_value_text_jet4() {
495 let raw = [0x41, 0x00, 0x62, 0x00];
497 let val = decode_prop_value(0x0A, &raw, "x", false);
498 assert_eq!(val, Value::Text("Ab".to_string()));
499 }
500
501 #[test]
502 fn decode_prop_value_text_jet3() {
503 let raw = b"Hello";
504 let val = decode_prop_value(0x0A, raw, "x", true);
505 assert_eq!(val, Value::Text("Hello".to_string()));
506 }
507
508 #[test]
509 fn decode_prop_value_long() {
510 let raw = 42i32.to_le_bytes();
511 let val = decode_prop_value(0x04, &raw, "x", false);
512 assert_eq!(val, Value::Long(42));
513 }
514
515 #[test]
516 fn decode_prop_value_int() {
517 let raw = (-7i16).to_le_bytes();
518 let val = decode_prop_value(0x03, &raw, "x", false);
519 assert_eq!(val, Value::Int(-7));
520 }
521
522 #[test]
523 fn decode_prop_value_binary_as_guid() {
524 let guid_bytes: [u8; 16] = [
525 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
526 0x0F, 0x10,
527 ];
528 let val = decode_prop_value(0x09, &guid_bytes, "GUID", false);
529 assert_eq!(
530 val,
531 Value::Guid("{04030201-0605-0807-090A-0B0C0D0E0F10}".to_string())
532 );
533 }
534
535 #[test]
536 fn decode_prop_value_binary_non_guid() {
537 let raw = [0x01, 0x02, 0x03];
538 let val = decode_prop_value(0x09, &raw, "SomeField", false);
539 assert_eq!(val, Value::Binary(vec![0x01, 0x02, 0x03]));
540 }
541
542 #[test]
543 fn decode_prop_value_memo_as_text() {
544 let raw = [0x4F, 0x00, 0x6B, 0x00];
546 let val = decode_prop_value(0x0C, &raw, "x", false);
547 assert_eq!(val, Value::Text("Ok".to_string()));
548 }
549
550 #[test]
553 fn parse_value_chunk_with_name() {
554 let names = vec!["Required".to_string(), "Description".to_string()];
555
556 let mut chunk = Vec::new();
561 chunk.extend_from_slice(&12u32.to_le_bytes());
563 chunk.extend_from_slice(&6u16.to_le_bytes());
565 chunk.extend_from_slice(&[0x43, 0x00, 0x6F, 0x00, 0x6C, 0x00]);
567
568 chunk.extend_from_slice(&9u16.to_le_bytes()); chunk.push(0x01); chunk.push(0x01); chunk.extend_from_slice(&0u16.to_le_bytes()); chunk.extend_from_slice(&1u16.to_le_bytes()); chunk.push(0x01); let map = parse_value_chunk(&chunk, 0x0001, &names, false).unwrap();
579 assert_eq!(map.map_type, PropMapType::Column);
580 assert_eq!(map.name, "Col");
581 assert_eq!(map.properties.len(), 1);
582 assert_eq!(map.properties[0].name, "Required");
583 assert_eq!(map.properties[0].value, Value::Bool(true));
584 assert!(map.properties[0].ddl);
585 }
586
587 #[test]
588 fn parse_value_chunk_no_name() {
589 let names = vec!["AccessVersion".to_string()];
590
591 let mut chunk = Vec::new();
593 chunk.extend_from_slice(&4u32.to_le_bytes());
594
595 let text_bytes = [0x30, 0x00, 0x38, 0x00, 0x2E, 0x00, 0x35, 0x00, 0x30, 0x00];
598 let val_len: u16 = 8 + text_bytes.len() as u16;
599 chunk.extend_from_slice(&val_len.to_le_bytes());
600 chunk.push(0x00); chunk.push(0x0A); chunk.extend_from_slice(&0u16.to_le_bytes()); chunk.extend_from_slice(&(text_bytes.len() as u16).to_le_bytes());
604 chunk.extend_from_slice(&text_bytes);
605
606 let map = parse_value_chunk(&chunk, 0x0000, &names, false).unwrap();
607 assert_eq!(map.map_type, PropMapType::Default);
608 assert_eq!(map.name, "");
609 assert_eq!(map.properties.len(), 1);
610 assert_eq!(map.properties[0].name, "AccessVersion");
611 assert_eq!(map.properties[0].value, Value::Text("08.50".to_string()));
612 assert!(!map.properties[0].ddl);
613 }
614
615 #[test]
618 fn parse_lvprop_jet4_full() {
619 let mut blob = Vec::new();
622
623 blob.extend_from_slice(b"MR2\0");
625
626 let mut name_payload = Vec::new();
629 let title_utf16 = [0x54, 0x00, 0x69, 0x00, 0x74, 0x00, 0x6C, 0x00, 0x65, 0x00]; name_payload.extend_from_slice(&(title_utf16.len() as u16).to_le_bytes());
631 name_payload.extend_from_slice(&title_utf16);
632
633 let name_chunk_len = 6 + name_payload.len();
634 blob.extend_from_slice(&(name_chunk_len as u32).to_le_bytes());
635 blob.extend_from_slice(&0x0080u16.to_le_bytes());
636 blob.extend_from_slice(&name_payload);
637
638 let mut value_payload = Vec::new();
640 value_payload.extend_from_slice(&4u32.to_le_bytes());
642
643 let test_utf16 = [0x54, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00]; let entry_len: u16 = 8 + test_utf16.len() as u16;
646 value_payload.extend_from_slice(&entry_len.to_le_bytes());
647 value_payload.push(0x00); value_payload.push(0x0A); value_payload.extend_from_slice(&0u16.to_le_bytes()); value_payload.extend_from_slice(&(test_utf16.len() as u16).to_le_bytes());
651 value_payload.extend_from_slice(&test_utf16);
652
653 let value_chunk_len = 6 + value_payload.len();
654 blob.extend_from_slice(&(value_chunk_len as u32).to_le_bytes());
655 blob.extend_from_slice(&0x0000u16.to_le_bytes());
656 blob.extend_from_slice(&value_payload);
657
658 let maps = parse_lvprop(&blob, false).unwrap();
659 assert_eq!(maps.len(), 1);
660 assert_eq!(maps[0].map_type, PropMapType::Default);
661 assert_eq!(maps[0].name, "");
662 assert_eq!(maps[0].properties.len(), 1);
663 assert_eq!(maps[0].properties[0].name, "Title");
664 assert_eq!(maps[0].properties[0].value, Value::Text("Test".to_string()));
665 }
666
667 #[test]
668 fn parse_lvprop_unknown_chunk_skipped() {
669 let mut blob = Vec::new();
671 blob.extend_from_slice(b"MR2\0");
672
673 let unknown_chunk_len: u32 = 10; blob.extend_from_slice(&unknown_chunk_len.to_le_bytes());
676 blob.extend_from_slice(&0x00FFu16.to_le_bytes());
677 blob.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
678
679 let name_chunk_len: u32 = 6; blob.extend_from_slice(&name_chunk_len.to_le_bytes());
682 blob.extend_from_slice(&0x0080u16.to_le_bytes());
683
684 let maps = parse_lvprop(&blob, false).unwrap();
685 assert!(maps.is_empty());
686 }
687
688 fn test_data_path(relative: &str) -> Option<std::path::PathBuf> {
691 let manifest_dir = env!("CARGO_MANIFEST_DIR");
692 let path = std::path::PathBuf::from(manifest_dir)
693 .join("../../testdata")
694 .join(relative);
695 if path.exists() {
696 Some(path)
697 } else {
698 None
699 }
700 }
701
702 macro_rules! skip_if_missing {
703 ($path:expr) => {
704 match test_data_path($path) {
705 Some(p) => p,
706 None => {
707 eprintln!("SKIP: test data not found: {}", $path);
708 return;
709 }
710 }
711 };
712 }
713
714 fn assert_has_properties(props: &ObjectProperties) {
715 assert!(
716 !props.maps.is_empty(),
717 "object '{}' should have at least one property map",
718 props.object_name
719 );
720 let total_props: usize = props.maps.iter().map(|m| m.properties.len()).sum();
722 assert!(
723 total_props > 0,
724 "object '{}' should have at least one property",
725 props.object_name
726 );
727 }
728
729 #[test]
730 fn jet3_table_properties() {
731 let path = skip_if_missing!("V1997/testV1997.mdb");
732 let mut reader = PageReader::open(&path).unwrap();
733 let props = read_object_properties(&mut reader, "Table1").unwrap();
734 assert_eq!(props.object_name, "Table1");
735 assert_has_properties(&props);
736 }
737
738 #[test]
739 fn jet4_table_properties() {
740 let path = skip_if_missing!("V2003/testV2003.mdb");
741 let mut reader = PageReader::open(&path).unwrap();
742 let props = read_object_properties(&mut reader, "Table1").unwrap();
743 assert_eq!(props.object_name, "Table1");
744 assert_has_properties(&props);
745
746 let default_map = props
748 .maps
749 .iter()
750 .find(|m| m.map_type == PropMapType::Default);
751 assert!(default_map.is_some(), "should have a Default property map");
752
753 let default_map = default_map.unwrap();
755 let guid = default_map.properties.iter().find(|p| p.name == "GUID");
756 assert!(guid.is_some(), "should have GUID property");
757 }
758
759 #[test]
760 fn ace12_table_properties() {
761 let path = skip_if_missing!("V2007/testV2007.accdb");
762 let mut reader = PageReader::open(&path).unwrap();
763 let props = read_object_properties(&mut reader, "Table1").unwrap();
764 assert_eq!(props.object_name, "Table1");
765 assert_has_properties(&props);
766 }
767
768 #[test]
769 fn ace14_table_properties() {
770 let path = skip_if_missing!("V2010/testV2010.accdb");
771 let mut reader = PageReader::open(&path).unwrap();
772 let props = read_object_properties(&mut reader, "Table1").unwrap();
773 assert_eq!(props.object_name, "Table1");
774 assert_has_properties(&props);
775 }
776
777 #[test]
778 fn nonexistent_object_returns_empty() {
779 let path = skip_if_missing!("V2003/testV2003.mdb");
780 let mut reader = PageReader::open(&path).unwrap();
781 let props = read_object_properties(&mut reader, "NoSuchObject_XYZ_12345").unwrap();
782 assert!(props.maps.is_empty());
783 }
784
785 #[test]
788 fn decode_prop_value_money() {
789 let raw = 10_000i64.to_le_bytes();
790 let val = decode_prop_value(0x05, &raw, "test", false);
791 assert_eq!(val, Value::Money("1.0000".to_string()));
792 }
793
794 #[test]
795 fn decode_prop_value_money_short() {
796 let val = decode_prop_value(0x05, &[0x01, 0x02, 0x03], "test", false);
797 assert_eq!(val, Value::Null);
798 }
799
800 #[test]
801 fn decode_prop_value_float() {
802 let raw = 1.5f32.to_le_bytes();
803 let val = decode_prop_value(0x06, &raw, "test", false);
804 assert!(matches!(val, Value::Float(v) if (v - 1.5).abs() < f32::EPSILON));
805 }
806
807 #[test]
808 fn decode_prop_value_float_short() {
809 let val = decode_prop_value(0x06, &[0x01, 0x02], "test", false);
810 assert_eq!(val, Value::Null);
811 }
812
813 #[test]
814 fn decode_prop_value_double() {
815 let raw = 3.125f64.to_le_bytes();
816 let val = decode_prop_value(0x07, &raw, "test", false);
817 assert!(matches!(val, Value::Double(v) if (v - 3.125).abs() < f64::EPSILON));
818 }
819
820 #[test]
821 fn decode_prop_value_double_short() {
822 let val = decode_prop_value(0x07, &[0x01, 0x02, 0x03, 0x04], "test", false);
823 assert_eq!(val, Value::Null);
824 }
825
826 #[test]
827 fn decode_prop_value_timestamp() {
828 let raw = 37623.0f64.to_le_bytes();
829 let val = decode_prop_value(0x08, &raw, "test", false);
830 assert!(matches!(val, Value::Timestamp(v) if (v - 37623.0).abs() < f64::EPSILON));
831 }
832
833 #[test]
834 fn decode_prop_value_timestamp_short() {
835 let val = decode_prop_value(0x08, &[0x01], "test", false);
836 assert_eq!(val, Value::Null);
837 }
838
839 #[test]
840 fn decode_prop_value_ole_binary() {
841 let raw = vec![0xDE, 0xAD, 0xBE, 0xEF];
842 let val = decode_prop_value(0x0B, &raw, "test", false);
843 assert_eq!(val, Value::Binary(raw));
844 }
845
846 #[test]
847 fn decode_prop_value_guid_16bytes() {
848 let guid_bytes: [u8; 16] = [
849 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
850 0x0F, 0x10,
851 ];
852 let val = decode_prop_value(0x0F, &guid_bytes, "test", false);
853 assert_eq!(
854 val,
855 Value::Guid("{04030201-0605-0807-090A-0B0C0D0E0F10}".to_string())
856 );
857 }
858
859 #[test]
860 fn decode_prop_value_guid_non16bytes() {
861 let raw = vec![0x01, 0x02, 0x03];
862 let val = decode_prop_value(0x0F, &raw, "test", false);
863 assert_eq!(val, Value::Binary(raw));
864 }
865
866 #[test]
867 fn decode_prop_value_byte_empty() {
868 let val = decode_prop_value(0x02, &[], "test", false);
869 assert_eq!(val, Value::Null);
870 }
871
872 #[test]
873 fn decode_prop_value_byte_valid() {
874 let val = decode_prop_value(0x02, &[42], "test", false);
875 assert_eq!(val, Value::Byte(42));
876 }
877
878 #[test]
879 fn decode_prop_value_int_short() {
880 let val = decode_prop_value(0x03, &[0x01], "test", false);
881 assert_eq!(val, Value::Null);
882 }
883
884 #[test]
885 fn decode_prop_value_long_short() {
886 let val = decode_prop_value(0x04, &[0x01, 0x02], "test", false);
887 assert_eq!(val, Value::Null);
888 }
889
890 #[test]
891 fn decode_prop_value_unknown_type() {
892 let raw = vec![0xFF, 0xFE];
893 let val = decode_prop_value(0xFF, &raw, "test", false);
894 assert_eq!(val, Value::Binary(raw));
895 }
896
897 #[test]
898 fn decode_prop_value_bool_empty() {
899 let val = decode_prop_value(0x01, &[], "test", false);
900 assert_eq!(val, Value::Bool(false));
901 }
902
903 #[test]
904 fn decode_prop_value_memo_jet3() {
905 let raw = b"Hello Jet3";
906 let val = decode_prop_value(0x0C, raw, "test", true);
907 assert_eq!(val, Value::Text("Hello Jet3".to_string()));
908 }
909
910 #[test]
911 fn decode_prop_value_text_invalid_utf16() {
912 let raw = vec![0x41, 0x00, 0x42];
914 let val = decode_prop_value(0x0A, &raw, "test", false);
915 assert_eq!(val, Value::Binary(raw));
916 }
917
918 #[test]
919 fn decode_prop_value_memo_invalid_utf16() {
920 let raw = vec![0xFF];
921 let val = decode_prop_value(0x0C, &raw, "test", false);
922 assert_eq!(val, Value::Binary(raw));
923 }
924
925 #[test]
928 fn parse_value_chunk_type_additional() {
929 let names = vec!["Prop1".to_string()];
930 let mut chunk = Vec::new();
931 chunk.extend_from_slice(&4u32.to_le_bytes()); let raw = 99i32.to_le_bytes();
935 let val_len: u16 = 8 + raw.len() as u16;
936 chunk.extend_from_slice(&val_len.to_le_bytes());
937 chunk.push(0x00); chunk.push(0x04); chunk.extend_from_slice(&0u16.to_le_bytes());
940 chunk.extend_from_slice(&(raw.len() as u16).to_le_bytes());
941 chunk.extend_from_slice(&raw);
942
943 let map = parse_value_chunk(&chunk, 0x0002, &names, false).unwrap();
944 assert_eq!(map.map_type, PropMapType::Additional);
945 assert_eq!(map.properties[0].value, Value::Long(99));
946 }
947
948 #[test]
949 fn parse_value_chunk_too_short() {
950 let names: Vec<String> = vec![];
951 let chunk = vec![0x01, 0x02]; let map = parse_value_chunk(&chunk, 0x0000, &names, false).unwrap();
953 assert!(map.properties.is_empty());
954 }
955}