Skip to main content

jetdb/
prop.rs

1//! Object property reading from MSysObjects.LvProp blobs.
2
3use 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// ---------------------------------------------------------------------------
11// Public data structures
12// ---------------------------------------------------------------------------
13
14/// Property map classification based on chunk type.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PropMapType {
17    /// Table-level properties (chunk type 0x0000).
18    Default,
19    /// Column-level properties (chunk type 0x0001).
20    Column,
21    /// Additional properties (chunk type 0x0002).
22    Additional,
23}
24
25/// A single property entry.
26#[derive(Debug, Clone)]
27pub struct Property {
28    pub name: String,
29    pub value: Value,
30    pub ddl: bool,
31}
32
33/// A named group of properties (table-level or per-column).
34#[derive(Debug, Clone)]
35pub struct PropertyMap {
36    pub map_type: PropMapType,
37    /// Map name (empty for table-level, column name for column-level).
38    pub name: String,
39    pub properties: Vec<Property>,
40}
41
42/// All properties for a single database object.
43#[derive(Debug, Clone)]
44pub struct ObjectProperties {
45    pub object_name: String,
46    pub maps: Vec<PropertyMap>,
47}
48
49// ---------------------------------------------------------------------------
50// Public API
51// ---------------------------------------------------------------------------
52
53/// Read properties for a named object from MSysObjects.LvProp.
54pub 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    // Locate Name and LvProp column indices
64    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    // Find the matching row
87    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    // Object not found — return empty
114    Ok(ObjectProperties {
115        object_name: object_name.to_string(),
116        maps: Vec::new(),
117    })
118}
119
120// ---------------------------------------------------------------------------
121// Internal parse functions
122// ---------------------------------------------------------------------------
123
124/// Magic bytes for Jet3 LvProp header.
125const HEADER_JET3: &[u8; 4] = b"KKD\0";
126/// Magic bytes for Jet4/ACE LvProp header.
127const HEADER_JET4: &[u8; 4] = b"MR2\0";
128
129/// Chunk type: name dictionary.
130const CHUNK_NAME_LIST: u16 = 0x0080;
131
132/// Validate the 4-byte LvProp header magic bytes.
133fn 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
148/// Parse the entire LvProp binary blob.
149pub(crate) fn parse_lvprop(data: &[u8], is_jet3: bool) -> Result<Vec<PropertyMap>, FileError> {
150    validate_header(data)?;
151
152    let mut offset = 4; // skip header
153    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            // Truncated chunk — stop parsing rather than error
167            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                // Unknown chunk type — skip
182            }
183        }
184
185        offset += chunk_len;
186    }
187
188    Ok(maps)
189}
190
191/// Parse a name-list chunk (type 0x0080).
192fn 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
215/// Parse a value chunk (type 0x0000 / 0x0001 / 0x0002).
216fn 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    // Sub-header: name_block_len (4 bytes)
230    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    // Extract block name
241    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    // Property entries start after name_block_len bytes
255    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        // Advance by val_len (entry total length)
286        pos += val_len;
287    }
288
289    Ok(PropertyMap {
290        map_type,
291        name: block_name,
292        properties,
293    })
294}
295
296/// Decode a property value based on its data type byte.
297fn decode_prop_value(data_type: u8, raw: &[u8], prop_name: &str, is_jet3: bool) -> Value {
298    match data_type {
299        // Bool (0x01): 1 byte, 0 = false, non-zero = true
300        0x01 => {
301            if raw.is_empty() {
302                Value::Bool(false)
303            } else {
304                Value::Bool(raw[0] != 0)
305            }
306        }
307        // Byte (0x02)
308        0x02 => {
309            if raw.is_empty() {
310                Value::Null
311            } else {
312                Value::Byte(raw[0])
313            }
314        }
315        // Int (0x03): 2 bytes LE
316        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        // Long (0x04): 4 bytes LE
324        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        // Money (0x05): 8 bytes LE
332        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        // Float (0x06): 4 bytes LE
341        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        // Double (0x07): 8 bytes LE
349        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        // Timestamp (0x08): 8 bytes LE f64
357        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        // Binary (0x09): special case for GUID
365        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        // Text (0x0A): non-compressed decode
373        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        // OLE (0x0B): treat as binary
384        0x0B => Value::Binary(raw.to_vec()),
385        // Memo (0x0C): decode as text
386        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        // Guid (0x0F): format as GUID if 16 bytes
397        0x0F => {
398            if raw.len() == 16 {
399                Value::Guid(format_guid(raw))
400            } else {
401                Value::Binary(raw.to_vec())
402            }
403        }
404        // Unknown types: preserve as binary
405        _ => Value::Binary(raw.to_vec()),
406    }
407}
408
409// ---------------------------------------------------------------------------
410// Tests
411// ---------------------------------------------------------------------------
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    // -- validate_header ------------------------------------------------------
418
419    #[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    // -- parse_name_list ------------------------------------------------------
444
445    #[test]
446    fn parse_name_list_jet3() {
447        // Two Latin-1 names: "Foo" (3 bytes), "Bar" (3 bytes)
448        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        // Two UTF-16LE names: "Hi" (4 bytes), "Go" (4 bytes)
461        let mut data = Vec::new();
462        // "Hi"
463        data.extend_from_slice(&4u16.to_le_bytes());
464        data.extend_from_slice(&[0x48, 0x00, 0x69, 0x00]);
465        // "Go"
466        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    // -- decode_prop_value ----------------------------------------------------
480
481    #[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        // "Ab" in UTF-16LE
496        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        // Memo (0x0C) in UTF-16LE: "Ok"
545        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    // -- parse_value_chunk ----------------------------------------------------
551
552    #[test]
553    fn parse_value_chunk_with_name() {
554        let names = vec!["Required".to_string(), "Description".to_string()];
555
556        // Build chunk_data:
557        // Sub-header: name_block_len = 4 + 2 + 6 = 12 (includes block name "Col" in UTF-16LE)
558        // Block name: "Col" = [0x43,0x00, 0x6F,0x00, 0x6C,0x00] (6 bytes)
559        // name_block_len = 12 (4 + 2 + 6)
560        let mut chunk = Vec::new();
561        // name_block_len
562        chunk.extend_from_slice(&12u32.to_le_bytes());
563        // block name length
564        chunk.extend_from_slice(&6u16.to_le_bytes());
565        // block name "Col" in UTF-16LE
566        chunk.extend_from_slice(&[0x43, 0x00, 0x6F, 0x00, 0x6C, 0x00]);
567
568        // Property entry: Bool "Required" = true
569        // val_len = 8 + 1 = 9, ddl_flag = 1, data_type = 0x01, name_idx = 0,
570        // data_size = 1, value = [0x01]
571        chunk.extend_from_slice(&9u16.to_le_bytes()); // val_len
572        chunk.push(0x01); // ddl_flag
573        chunk.push(0x01); // data_type = Bool
574        chunk.extend_from_slice(&0u16.to_le_bytes()); // name_idx = 0 -> "Required"
575        chunk.extend_from_slice(&1u16.to_le_bytes()); // data_size = 1
576        chunk.push(0x01); // value = true
577
578        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        // name_block_len = 4 (no block name)
592        let mut chunk = Vec::new();
593        chunk.extend_from_slice(&4u32.to_le_bytes());
594
595        // Property entry: Text "AccessVersion" = "08.50" in UTF-16LE
596        // "08.50" = 5 chars = 10 bytes
597        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); // ddl_flag = false
601        chunk.push(0x0A); // data_type = Text
602        chunk.extend_from_slice(&0u16.to_le_bytes()); // name_idx = 0
603        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    // -- parse_lvprop full ----------------------------------------------------
616
617    #[test]
618    fn parse_lvprop_jet4_full() {
619        // Build a complete LvProp blob:
620        // [header(4)] [name_list_chunk] [value_chunk]
621        let mut blob = Vec::new();
622
623        // Header: MR2\0
624        blob.extend_from_slice(b"MR2\0");
625
626        // -- Name list chunk (type=0x0080) --
627        // Names: "Title" in UTF-16LE (10 bytes)
628        let mut name_payload = Vec::new();
629        let title_utf16 = [0x54, 0x00, 0x69, 0x00, 0x74, 0x00, 0x6C, 0x00, 0x65, 0x00]; // "Title"
630        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        // -- Value chunk (type=0x0000, table-level) --
639        let mut value_payload = Vec::new();
640        // name_block_len = 4 (no block name)
641        value_payload.extend_from_slice(&4u32.to_le_bytes());
642
643        // Property: Text "Title" = "Test" in UTF-16LE
644        let test_utf16 = [0x54, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00]; // "Test"
645        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); // ddl_flag
648        value_payload.push(0x0A); // data_type = Text
649        value_payload.extend_from_slice(&0u16.to_le_bytes()); // name_idx = 0
650        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        // Header + unknown chunk (type=0x00FF) + name list chunk
670        let mut blob = Vec::new();
671        blob.extend_from_slice(b"MR2\0");
672
673        // Unknown chunk: type=0x00FF, payload = 4 bytes of garbage
674        let unknown_chunk_len: u32 = 10; // 6 header + 4 payload
675        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        // Name list chunk (empty)
680        let name_chunk_len: u32 = 6; // header only, no names
681        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    // -- Integration tests with real files ------------------------------------
689
690    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        // At least one map should have properties
721        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        // Default マップ (テーブルプロパティ) が存在すること
747        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        // GUID プロパティが存在すること
754        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    // -- decode_prop_value additional types ------------------------------------
786
787    #[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        // Odd number of bytes cannot be valid UTF-16LE
913        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    // -- parse_value_chunk additional type -------------------------------------
926
927    #[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()); // name_block_len = 4
932
933        // Property entry: Long "Prop1" = 99
934        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); // ddl_flag
938        chunk.push(0x04); // data_type = Long
939        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]; // chunk_data < 4 bytes
952        let map = parse_value_chunk(&chunk, 0x0000, &names, false).unwrap();
953        assert!(map.properties.is_empty());
954    }
955}