1pub fn detect_physical_memory() -> Option<String> {
7 #[cfg(target_os = "linux")]
8 return detect_linux();
9
10 #[cfg(target_os = "macos")]
11 return detect_macos();
12
13 #[cfg(target_os = "windows")]
14 return detect_windows();
15
16 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
17 return None;
18}
19
20#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
22#[derive(Debug, PartialEq)]
23pub struct DimmSlot {
24 pub size_mb: u64,
25 pub mem_type: String,
26 pub speed_mt: Option<u64>,
28 pub configured_speed_mt: Option<u64>,
32}
33
34#[cfg(target_os = "linux")]
36pub fn parse_dmidecode_type17(output: &str) -> Vec<DimmSlot> {
37 let mut slots = Vec::new();
38 let mut size_mb: Option<u64> = None;
39 let mut mem_type = String::new();
40 let mut speed_mt: Option<u64> = None;
41 let mut configured_speed_mt: Option<u64> = None;
42
43 for line in output.lines() {
44 let trimmed = line.trim();
45
46 if trimmed == "Memory Device" {
47 if let Some(mb) = size_mb.take() {
49 if mb > 0 {
50 slots.push(DimmSlot {
51 size_mb: mb,
52 mem_type: mem_type.clone(),
53 speed_mt,
54 configured_speed_mt,
55 });
56 }
57 }
58 mem_type.clear();
59 speed_mt = None;
60 configured_speed_mt = None;
61 } else if let Some(rest) = trimmed.strip_prefix("Size:") {
62 let rest = rest.trim();
63 if rest.contains("No Module") || rest == "Unknown" {
64 size_mb = Some(0);
65 } else if let Some(n) = rest
66 .strip_suffix(" GiB")
67 .or_else(|| rest.strip_suffix(" GB"))
68 {
69 size_mb = n.trim().parse::<u64>().ok().map(|g| g * 1024);
70 } else if let Some(n) = rest
71 .strip_suffix(" MiB")
72 .or_else(|| rest.strip_suffix(" MB"))
73 {
74 size_mb = n.trim().parse::<u64>().ok();
75 }
76 } else if let Some(rest) = trimmed.strip_prefix("Type:") {
77 let t = rest.trim();
78 if t != "Unknown" && !t.is_empty() {
79 mem_type = t.to_string();
80 }
81 } else if let Some(rest) = trimmed.strip_prefix("Configured Memory Speed:") {
82 let rest = rest.trim();
85 if let Some(mt_str) = rest.strip_suffix(" MT/s") {
86 configured_speed_mt = mt_str.trim().parse::<u64>().ok();
87 }
88 } else if let Some(rest) = trimmed.strip_prefix("Speed:") {
89 let rest = rest.trim();
91 if let Some(mt_str) = rest.strip_suffix(" MT/s") {
92 speed_mt = mt_str.trim().parse::<u64>().ok();
93 }
94 }
95 }
96
97 if let Some(mb) = size_mb {
99 if mb > 0 {
100 slots.push(DimmSlot {
101 size_mb: mb,
102 mem_type,
103 speed_mt,
104 configured_speed_mt,
105 });
106 }
107 }
108
109 slots
110}
111
112#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
114pub fn format_dimm_slots(slots: &[DimmSlot]) -> Option<String> {
115 if slots.is_empty() {
116 return None;
117 }
118
119 #[derive(PartialEq, Eq, Hash)]
121 struct Key {
122 size_mb: u64,
123 mem_type: String,
124 speed_mt: Option<u64>,
125 configured_speed_mt: Option<u64>,
126 }
127
128 let mut groups: Vec<(Key, usize)> = Vec::new();
129 for slot in slots {
130 let key = Key {
131 size_mb: slot.size_mb,
132 mem_type: slot.mem_type.clone(),
133 speed_mt: slot.speed_mt,
134 configured_speed_mt: slot.configured_speed_mt,
135 };
136 if let Some(entry) = groups.iter_mut().find(|(k, _)| k == &key) {
137 entry.1 += 1;
138 } else {
139 groups.push((key, 1));
140 }
141 }
142
143 let parts: Vec<String> = groups
144 .iter()
145 .map(|(key, count)| {
146 let size_str = if key.size_mb >= 1024 {
147 format!("{} GB", key.size_mb / 1024)
148 } else {
149 format!("{} MB", key.size_mb)
150 };
151
152 let mut s = if *count > 1 {
153 format!("{}× {}", count, size_str)
154 } else {
155 size_str
156 };
157
158 if !key.mem_type.is_empty() {
159 s.push(' ');
160 s.push_str(&key.mem_type);
161 }
162
163 match (key.speed_mt, key.configured_speed_mt) {
164 (Some(rated), Some(running)) if running != rated => {
165 s.push_str(&format!(" {} MT/s (rated {} MT/s)", running, rated));
166 }
167 (Some(mt), _) => {
168 s.push_str(&format!(" {} MT/s", mt));
169 }
170 (None, Some(running)) => {
171 s.push_str(&format!(" {} MT/s", running));
172 }
173 (None, None) => {}
174 }
175
176 s
177 })
178 .collect();
179
180 Some(parts.join(", "))
181}
182
183#[cfg(target_os = "linux")]
184fn detect_linux() -> Option<String> {
185 let candidates = ["dmidecode", "/usr/bin/dmidecode", "/usr/sbin/dmidecode"];
187 for cmd in candidates {
188 let Ok(output) = std::process::Command::new(cmd)
189 .args(["--type", "17"])
190 .output()
191 else {
192 continue;
193 };
194 if !output.status.success() {
195 continue;
196 }
197 let text = String::from_utf8_lossy(&output.stdout);
198 let slots = parse_dmidecode_type17(&text);
199 return format_dimm_slots(&slots);
200 }
201 None
202}
203
204#[cfg(target_os = "macos")]
205fn detect_macos() -> Option<String> {
206 let output = std::process::Command::new("system_profiler")
207 .args(["SPMemoryDataType", "-detailLevel", "basic"])
208 .output()
209 .ok()?;
210
211 if !output.status.success() {
212 return None;
213 }
214
215 let text = String::from_utf8_lossy(&output.stdout);
216 parse_system_profiler_memory(&text)
217}
218
219#[cfg(target_os = "macos")]
221pub fn parse_system_profiler_memory(text: &str) -> Option<String> {
222 let mut slots: Vec<DimmSlot> = Vec::new();
235 let mut current_size_mb: Option<u64> = None;
236 let mut current_type = String::new();
237 let mut current_speed: Option<u64> = None;
238 let mut in_slot = false;
239
240 for line in text.lines() {
241 let trimmed = line.trim();
242
243 if (trimmed.contains("DIMM") || trimmed.contains("BANK") || trimmed.contains("Slot"))
245 && trimmed.ends_with(':')
246 {
247 if in_slot {
248 if let Some(mb) = current_size_mb.take() {
249 if mb > 0 {
250 slots.push(DimmSlot {
251 size_mb: mb,
252 mem_type: current_type.clone(),
253 speed_mt: current_speed,
254 configured_speed_mt: None,
255 });
256 }
257 }
258 current_type.clear();
259 current_speed = None;
260 }
261 in_slot = true;
262 continue;
263 }
264
265 let size_rest = trimmed.strip_prefix("Size:").or_else(|| {
267 trimmed
268 .strip_prefix("Memory:")
269 .filter(|s| !s.trim().is_empty())
270 });
271 if let Some(rest) = size_rest {
272 let rest = rest.trim();
273 if rest.contains("Empty") || rest == "Unknown" {
274 current_size_mb = Some(0);
275 } else if let Some(gb_str) = rest.strip_suffix(" GB") {
276 current_size_mb = gb_str.trim().parse::<u64>().ok().map(|g| g * 1024);
277 } else if let Some(mb_str) = rest.strip_suffix(" MB") {
278 current_size_mb = mb_str.trim().parse::<u64>().ok();
279 }
280 } else if let Some(rest) = trimmed.strip_prefix("Type:") {
281 let t = rest.trim();
282 if t != "Unknown" && !t.is_empty() {
283 current_type = t.to_string();
284 }
285 } else if let Some(rest) = trimmed.strip_prefix("Speed:") {
286 let rest = rest.trim();
287 if let Some(mt_str) = rest.strip_suffix(" MT/s") {
288 current_speed = mt_str.trim().parse::<u64>().ok();
289 } else if let Some(mhz_str) = rest.strip_suffix(" MHz") {
290 current_speed = mhz_str.trim().parse::<u64>().ok().map(|mhz| mhz * 2);
292 }
293 }
294 }
295
296 if let Some(mb) = current_size_mb {
298 if mb > 0 {
299 slots.push(DimmSlot {
300 size_mb: mb,
301 mem_type: current_type,
302 speed_mt: current_speed,
303 configured_speed_mt: None,
304 });
305 }
306 }
307
308 format_dimm_slots(&slots)
312}
313
314#[cfg(target_os = "windows")]
322fn detect_windows() -> Option<String> {
323 if let Some(table) = win_ffi::read_smbios_table() {
324 let slots = parse_smbios_type17(&table);
325 if let Some(result) = format_dimm_slots(&slots) {
326 return Some(result);
327 }
328 }
329
330 let total_bytes = win_ffi::total_physical_memory()?;
333 if total_bytes == 0 {
334 return None;
335 }
336 let gb = total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
337 Some(format!("{:.0} GB (VM — DIMM info unavailable)", gb))
338}
339
340#[cfg(target_os = "windows")]
347pub fn parse_smbios_type17(data: &[u8]) -> Vec<DimmSlot> {
348 fn u16_at(s: &[u8], off: usize) -> Option<u16> {
350 s.get(off..off + 2)
351 .map(|b| u16::from_le_bytes([b[0], b[1]]))
352 }
353 fn u32_at(s: &[u8], off: usize) -> Option<u32> {
354 s.get(off..off + 4)
355 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
356 }
357
358 let mut slots = Vec::new();
359 let mut i = 0usize;
360 while i + 4 <= data.len() {
361 let typ = data[i];
362 let slen = data[i + 1] as usize; if slen < 4 || i + slen > data.len() {
364 break; }
366 if typ == 127 {
367 break; }
369 if typ == 17 {
370 let s = &data[i..i + slen];
371 if let Some(slot) = parse_type17_struct(s) {
372 slots.push(slot);
373 }
374 }
375 let mut j = i + slen;
377 loop {
378 if j + 1 >= data.len() {
379 j = data.len();
380 break;
381 }
382 if data[j] == 0 && data[j + 1] == 0 {
383 j += 2;
384 break;
385 }
386 j += 1;
387 }
388 i = j;
389 }
390
391 fn parse_type17_struct(s: &[u8]) -> Option<DimmSlot> {
394 let size_raw = u16_at(s, 0x0C)?;
397 if size_raw == 0 {
398 return None;
399 }
400 let size_mb = if size_raw == 0x7FFF {
401 (u32_at(s, 0x1C)? & 0x7FFF_FFFF) as u64
402 } else {
403 let val = (size_raw & 0x7FFF) as u64;
404 if size_raw & 0x8000 != 0 {
405 val / 1024
406 } else {
407 val
408 }
409 };
410 if size_mb == 0 {
411 return None;
412 }
413
414 let mem_type = byte_at(s, 0x12)
415 .map(|code| smbios_memory_type(u16::from(code)))
416 .unwrap_or_default();
417
418 let speed_mt = read_speed(s, 0x15, 0x54);
421 let configured_speed_mt = read_speed(s, 0x20, 0x58);
422
423 Some(DimmSlot {
424 size_mb,
425 mem_type,
426 speed_mt,
427 configured_speed_mt,
428 })
429 }
430
431 fn byte_at(s: &[u8], off: usize) -> Option<u8> {
433 s.get(off).copied()
434 }
435
436 fn read_speed(s: &[u8], word_off: usize, ext_off: usize) -> Option<u64> {
439 match u16_at(s, word_off) {
440 None | Some(0) => None,
441 Some(0xFFFF) => u32_at(s, ext_off).filter(|&v| v > 0).map(|v| v as u64),
442 Some(v) => Some(v as u64),
443 }
444 }
445
446 slots
447}
448
449#[cfg(target_os = "windows")]
451fn smbios_memory_type(code: u16) -> String {
452 match code {
453 20 => "DDR".to_string(),
454 21 => "DDR2".to_string(),
455 24 => "DDR3".to_string(),
456 26 => "DDR4".to_string(),
457 27 => "LPDDR".to_string(),
458 28 => "LPDDR2".to_string(),
459 29 => "LPDDR3".to_string(),
460 30 => "LPDDR4".to_string(),
461 34 => "DDR5".to_string(),
462 35 => "LPDDR5".to_string(),
463 _ => String::new(),
464 }
465}
466
467#[cfg(target_os = "windows")]
472mod win_ffi {
473 use std::ffi::c_void;
474 use std::mem::size_of;
475
476 const RSMB: u32 = u32::from_be_bytes(*b"RSMB");
478 const RAW_SMBIOS_HEADER_LEN: usize = 8;
481
482 #[repr(C)]
483 struct MemoryStatusEx {
484 length: u32,
485 memory_load: u32,
486 total_phys: u64,
487 avail_phys: u64,
488 total_page_file: u64,
489 avail_page_file: u64,
490 total_virtual: u64,
491 avail_virtual: u64,
492 avail_extended_virtual: u64,
493 }
494
495 extern "system" {
496 fn GetSystemFirmwareTable(
497 firmware_table_provider_signature: u32,
498 firmware_table_id: u32,
499 firmware_table_buffer: *mut c_void,
500 buffer_size: u32,
501 ) -> u32;
502
503 fn GlobalMemoryStatusEx(buffer: *mut MemoryStatusEx) -> i32;
504 }
505
506 pub fn read_smbios_table() -> Option<Vec<u8>> {
509 let needed = unsafe { GetSystemFirmwareTable(RSMB, 0, std::ptr::null_mut(), 0) };
511 if needed == 0 {
512 return None;
513 }
514 let mut buf = vec![0u8; needed as usize];
515 let written =
517 unsafe { GetSystemFirmwareTable(RSMB, 0, buf.as_mut_ptr() as *mut c_void, needed) };
518 if written == 0 || (written as usize) > buf.len() {
519 return None;
520 }
521 buf.truncate(written as usize);
522 if buf.len() < RAW_SMBIOS_HEADER_LEN {
523 return None;
524 }
525 let table_len = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
527 let end = RAW_SMBIOS_HEADER_LEN.checked_add(table_len)?;
528 let end = end.min(buf.len());
529 Some(buf[RAW_SMBIOS_HEADER_LEN..end].to_vec())
530 }
531
532 pub fn total_physical_memory() -> Option<u64> {
534 let mut status = MemoryStatusEx {
535 length: size_of::<MemoryStatusEx>() as u32,
536 memory_load: 0,
537 total_phys: 0,
538 avail_phys: 0,
539 total_page_file: 0,
540 avail_page_file: 0,
541 total_virtual: 0,
542 avail_virtual: 0,
543 avail_extended_virtual: 0,
544 };
545 let ok = unsafe { GlobalMemoryStatusEx(&mut status) };
547 if ok == 0 {
548 None
549 } else {
550 Some(status.total_phys)
551 }
552 }
553
554 #[cfg(test)]
555 mod layout {
556 use std::mem::size_of;
557
558 #[test]
560 fn ffi_struct_layout() {
561 assert_eq!(size_of::<super::MemoryStatusEx>(), 64);
562 }
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[cfg(target_os = "linux")]
571 #[test]
572 fn test_parse_dmidecode_two_slots() {
573 let input = r#"
574Memory Device
575 Size: 8 GB
576 Type: DDR5
577 Speed: 4800 MT/s
578
579Memory Device
580 Size: 8 GB
581 Type: DDR5
582 Speed: 4800 MT/s
583"#;
584 let slots = parse_dmidecode_type17(input);
585 assert_eq!(slots.len(), 2);
586 assert_eq!(slots[0].size_mb, 8192);
587 assert_eq!(slots[0].mem_type, "DDR5");
588 assert_eq!(slots[0].speed_mt, Some(4800));
589
590 let summary = format_dimm_slots(&slots).unwrap();
591 assert_eq!(summary, "2× 8 GB DDR5 4800 MT/s");
592 }
593
594 #[cfg(target_os = "linux")]
595 #[test]
596 fn test_parse_dmidecode_configured_speed_below_rated() {
597 let input = r#"
599Memory Device
600 Size: 16 GB
601 Type: DDR5
602 Speed: 6000 MT/s
603 Configured Memory Speed: 4800 MT/s
604
605Memory Device
606 Size: 16 GB
607 Type: DDR5
608 Speed: 6000 MT/s
609 Configured Memory Speed: 4800 MT/s
610"#;
611 let slots = parse_dmidecode_type17(input);
612 assert_eq!(slots[0].speed_mt, Some(6000));
613 assert_eq!(slots[0].configured_speed_mt, Some(4800));
614
615 let summary = format_dimm_slots(&slots).unwrap();
616 assert_eq!(summary, "2× 16 GB DDR5 4800 MT/s (rated 6000 MT/s)");
617 }
618
619 #[cfg(target_os = "linux")]
620 #[test]
621 fn test_parse_dmidecode_configured_speed_matches_rated() {
622 let input = r#"
624Memory Device
625 Size: 8 GB
626 Type: DDR5
627 Speed: 4800 MT/s
628 Configured Memory Speed: 4800 MT/s
629"#;
630 let slots = parse_dmidecode_type17(input);
631 let summary = format_dimm_slots(&slots).unwrap();
632 assert_eq!(summary, "8 GB DDR5 4800 MT/s");
633 }
634
635 #[cfg(target_os = "linux")]
636 #[test]
637 fn test_parse_dmidecode_gib_units() {
638 let input = r#"
640Memory Device
641 Size: 2 GiB
642 Type: LPDDR5
643 Speed: 6400 MT/s
644
645Memory Device
646 Size: 2 GiB
647 Type: LPDDR5
648 Speed: 6400 MT/s
649"#;
650 let slots = parse_dmidecode_type17(input);
651 assert_eq!(slots.len(), 2);
652 assert_eq!(slots[0].size_mb, 2048);
653 assert_eq!(slots[0].mem_type, "LPDDR5");
654 assert_eq!(slots[0].speed_mt, Some(6400));
655
656 let summary = format_dimm_slots(&slots).unwrap();
657 assert_eq!(summary, "2× 2 GB LPDDR5 6400 MT/s");
658 }
659
660 #[cfg(target_os = "linux")]
661 #[test]
662 fn test_parse_dmidecode_empty_slot() {
663 let input = r#"
664Memory Device
665 Size: No Module Installed
666 Type: Unknown
667
668Memory Device
669 Size: 16 GB
670 Type: DDR4
671 Speed: 3200 MT/s
672"#;
673 let slots = parse_dmidecode_type17(input);
674 assert_eq!(slots.len(), 1);
675 assert_eq!(slots[0].size_mb, 16384);
676 let summary = format_dimm_slots(&slots).unwrap();
677 assert_eq!(summary, "16 GB DDR4 3200 MT/s");
678 }
679
680 #[cfg(target_os = "linux")]
681 #[test]
682 fn test_parse_dmidecode_mixed_sizes() {
683 let input = r#"
684Memory Device
685 Size: 16 GB
686 Type: DDR5
687 Speed: 5600 MT/s
688
689Memory Device
690 Size: 32 GB
691 Type: DDR5
692 Speed: 5600 MT/s
693"#;
694 let slots = parse_dmidecode_type17(input);
695 assert_eq!(slots.len(), 2);
696 let summary = format_dimm_slots(&slots).unwrap();
697 assert!(summary.contains("16 GB DDR5 5600 MT/s"));
699 assert!(summary.contains("32 GB DDR5 5600 MT/s"));
700 }
701
702 #[cfg(target_os = "macos")]
703 #[test]
704 fn test_parse_system_profiler_apple_silicon() {
705 let input = r#"
706Memory:
707
708 Type: LPDDR5
709 Speed: 6400 MT/s
710 Size: 16 GB
711"#;
712 let result = parse_system_profiler_memory(input);
713 assert_eq!(result, Some("16 GB LPDDR5 6400 MT/s".to_string()));
714 }
715
716 #[cfg(target_os = "macos")]
717 #[test]
718 fn test_parse_system_profiler_multi_slot() {
719 let input = r#"
720Memory:
721
722 BANK 0/DIMM0:
723
724 Size: 16 GB
725 Type: DDR5
726 Speed: 4800 MT/s
727
728 BANK 1/DIMM0:
729
730 Size: 16 GB
731 Type: DDR5
732 Speed: 4800 MT/s
733"#;
734 let result = parse_system_profiler_memory(input);
735 assert_eq!(result, Some("2× 16 GB DDR5 4800 MT/s".to_string()));
736 }
737
738 #[cfg(target_os = "windows")]
742 fn make_type17(size_word: u16, type_code: u8, speed: u16, configured: u16) -> Vec<u8> {
743 let mut s = vec![0u8; 0x22];
744 s[0] = 17; s[1] = 0x22; s[2] = 0x00; s[3] = 0x11;
748 s[0x0C..0x0E].copy_from_slice(&size_word.to_le_bytes());
749 s[0x12] = type_code;
750 s[0x15..0x17].copy_from_slice(&speed.to_le_bytes()); s[0x20..0x22].copy_from_slice(&configured.to_le_bytes());
752 s.push(0);
754 s.push(0);
755 s
756 }
757
758 #[cfg(target_os = "windows")]
760 fn end_marker() -> Vec<u8> {
761 vec![127, 4, 0x00, 0x7F, 0, 0]
762 }
763
764 #[cfg(target_os = "windows")]
765 #[test]
766 fn test_parse_smbios_type17_ddr4_two_slots() {
767 let mut data = make_type17(0x2000, 26, 3200, 0);
769 data.extend(make_type17(0x2000, 26, 3200, 0));
770 data.extend(end_marker());
771 let slots = parse_smbios_type17(&data);
772 assert_eq!(slots.len(), 2);
773 assert_eq!(slots[0].size_mb, 8192);
774 assert_eq!(slots[0].mem_type, "DDR4");
775 assert_eq!(slots[0].speed_mt, Some(3200));
776 assert_eq!(format_dimm_slots(&slots).unwrap(), "2× 8 GB DDR4 3200 MT/s");
777 }
778
779 #[cfg(target_os = "windows")]
780 #[test]
781 fn test_parse_smbios_type17_ddr5_configured_below_rated() {
782 let mut data = make_type17(0x4000, 34, 6000, 4800);
785 data.extend(end_marker());
786 let slots = parse_smbios_type17(&data);
787 assert_eq!(slots.len(), 1);
788 assert_eq!(slots[0].speed_mt, Some(6000));
789 assert_eq!(slots[0].configured_speed_mt, Some(4800));
790 assert_eq!(
791 format_dimm_slots(&slots).unwrap(),
792 "16 GB DDR5 4800 MT/s (rated 6000 MT/s)"
793 );
794 }
795
796 #[cfg(target_os = "windows")]
797 #[test]
798 fn test_parse_smbios_type17_skips_empty_slot() {
799 let mut data = make_type17(0, 0, 0, 0);
803 data.extend(make_type17(0x4000, 34, 4800, 4800));
804 data.extend(end_marker());
805 let slots = parse_smbios_type17(&data);
806 assert_eq!(slots.len(), 1);
807 assert_eq!(slots[0].size_mb, 16384);
808 assert_eq!(format_dimm_slots(&slots).unwrap(), "16 GB DDR5 4800 MT/s");
809 }
810
811 #[cfg(target_os = "windows")]
812 #[test]
813 fn test_parse_smbios_type17_unknown_type_no_label() {
814 let mut data = make_type17(0x4000, 0, 0, 0);
816 data.extend(end_marker());
817 let slots = parse_smbios_type17(&data);
818 assert_eq!(format_dimm_slots(&slots).unwrap(), "16 GB");
819 }
820
821 #[cfg(target_os = "windows")]
822 #[test]
823 fn test_parse_smbios_type17_size_in_kb() {
824 let mut data = make_type17(0x8000 | 0x0400, 26, 3200, 0);
826 data.extend(end_marker());
827 let slots = parse_smbios_type17(&data);
828 assert_eq!(slots.len(), 1);
829 assert_eq!(slots[0].size_mb, 1);
830 }
831
832 #[cfg(target_os = "windows")]
833 #[test]
834 fn test_parse_smbios_type17_extended_size() {
835 let mut data = make_type17(0x7FFF, 34, 4800, 0);
837 data[0x1C..0x20].copy_from_slice(&65536u32.to_le_bytes());
838 data.extend(end_marker());
839 let slots = parse_smbios_type17(&data);
840 assert_eq!(slots.len(), 1);
841 assert_eq!(slots[0].size_mb, 65536);
842 }
843
844 #[cfg(target_os = "windows")]
845 #[test]
846 fn test_parse_smbios_type17_empty_table() {
847 assert!(parse_smbios_type17(&end_marker()).is_empty());
848 assert!(parse_smbios_type17(&[]).is_empty());
849 }
850}