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>,
27}
28
29#[cfg(target_os = "linux")]
31pub fn parse_dmidecode_type17(output: &str) -> Vec<DimmSlot> {
32 let mut slots = Vec::new();
33 let mut size_mb: Option<u64> = None;
34 let mut mem_type = String::new();
35 let mut speed_mt: Option<u64> = None;
36
37 for line in output.lines() {
38 let trimmed = line.trim();
39
40 if trimmed == "Memory Device" {
41 if let Some(mb) = size_mb.take() {
43 if mb > 0 {
44 slots.push(DimmSlot {
45 size_mb: mb,
46 mem_type: mem_type.clone(),
47 speed_mt,
48 });
49 }
50 }
51 mem_type.clear();
52 speed_mt = None;
53 } else if let Some(rest) = trimmed.strip_prefix("Size:") {
54 let rest = rest.trim();
55 if rest.contains("No Module") || rest == "Unknown" {
56 size_mb = Some(0);
57 } else if let Some(n) = rest
58 .strip_suffix(" GiB")
59 .or_else(|| rest.strip_suffix(" GB"))
60 {
61 size_mb = n.trim().parse::<u64>().ok().map(|g| g * 1024);
62 } else if let Some(n) = rest
63 .strip_suffix(" MiB")
64 .or_else(|| rest.strip_suffix(" MB"))
65 {
66 size_mb = n.trim().parse::<u64>().ok();
67 }
68 } else if let Some(rest) = trimmed.strip_prefix("Type:") {
69 let t = rest.trim();
70 if t != "Unknown" && !t.is_empty() {
71 mem_type = t.to_string();
72 }
73 } else if let Some(rest) = trimmed.strip_prefix("Speed:") {
74 let rest = rest.trim();
76 if let Some(mt_str) = rest.strip_suffix(" MT/s") {
77 speed_mt = mt_str.trim().parse::<u64>().ok();
78 }
79 }
80 }
81
82 if let Some(mb) = size_mb {
84 if mb > 0 {
85 slots.push(DimmSlot {
86 size_mb: mb,
87 mem_type,
88 speed_mt,
89 });
90 }
91 }
92
93 slots
94}
95
96#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
98pub fn format_dimm_slots(slots: &[DimmSlot]) -> Option<String> {
99 if slots.is_empty() {
100 return None;
101 }
102
103 #[derive(PartialEq, Eq, Hash)]
105 struct Key {
106 size_mb: u64,
107 mem_type: String,
108 speed_mt: Option<u64>,
109 }
110
111 let mut groups: Vec<(Key, usize)> = Vec::new();
112 for slot in slots {
113 let key = Key {
114 size_mb: slot.size_mb,
115 mem_type: slot.mem_type.clone(),
116 speed_mt: slot.speed_mt,
117 };
118 if let Some(entry) = groups.iter_mut().find(|(k, _)| k == &key) {
119 entry.1 += 1;
120 } else {
121 groups.push((key, 1));
122 }
123 }
124
125 let parts: Vec<String> = groups
126 .iter()
127 .map(|(key, count)| {
128 let size_str = if key.size_mb >= 1024 {
129 format!("{} GB", key.size_mb / 1024)
130 } else {
131 format!("{} MB", key.size_mb)
132 };
133
134 let mut s = if *count > 1 {
135 format!("{}× {}", count, size_str)
136 } else {
137 size_str
138 };
139
140 if !key.mem_type.is_empty() {
141 s.push(' ');
142 s.push_str(&key.mem_type);
143 }
144
145 if let Some(mt) = key.speed_mt {
146 s.push_str(&format!(" {} MT/s", mt));
147 }
148
149 s
150 })
151 .collect();
152
153 Some(parts.join(", "))
154}
155
156#[cfg(target_os = "linux")]
157fn detect_linux() -> Option<String> {
158 let candidates = ["dmidecode", "/usr/bin/dmidecode", "/usr/sbin/dmidecode"];
160 for cmd in candidates {
161 let Ok(output) = std::process::Command::new(cmd)
162 .args(["--type", "17"])
163 .output()
164 else {
165 continue;
166 };
167 if !output.status.success() {
168 continue;
169 }
170 let text = String::from_utf8_lossy(&output.stdout);
171 let slots = parse_dmidecode_type17(&text);
172 return format_dimm_slots(&slots);
173 }
174 None
175}
176
177#[cfg(target_os = "macos")]
178fn detect_macos() -> Option<String> {
179 let output = std::process::Command::new("system_profiler")
180 .args(["SPMemoryDataType", "-detailLevel", "basic"])
181 .output()
182 .ok()?;
183
184 if !output.status.success() {
185 return None;
186 }
187
188 let text = String::from_utf8_lossy(&output.stdout);
189 parse_system_profiler_memory(&text)
190}
191
192#[cfg(target_os = "macos")]
194pub fn parse_system_profiler_memory(text: &str) -> Option<String> {
195 let mut slots: Vec<DimmSlot> = Vec::new();
208 let mut current_size_mb: Option<u64> = None;
209 let mut current_type = String::new();
210 let mut current_speed: Option<u64> = None;
211 let mut in_slot = false;
212
213 for line in text.lines() {
214 let trimmed = line.trim();
215
216 if (trimmed.contains("DIMM") || trimmed.contains("BANK") || trimmed.contains("Slot"))
218 && trimmed.ends_with(':')
219 {
220 if in_slot {
221 if let Some(mb) = current_size_mb.take() {
222 if mb > 0 {
223 slots.push(DimmSlot {
224 size_mb: mb,
225 mem_type: current_type.clone(),
226 speed_mt: current_speed,
227 });
228 }
229 }
230 current_type.clear();
231 current_speed = None;
232 }
233 in_slot = true;
234 continue;
235 }
236
237 let size_rest = trimmed.strip_prefix("Size:").or_else(|| {
239 trimmed
240 .strip_prefix("Memory:")
241 .filter(|s| !s.trim().is_empty())
242 });
243 if let Some(rest) = size_rest {
244 let rest = rest.trim();
245 if rest.contains("Empty") || rest == "Unknown" {
246 current_size_mb = Some(0);
247 } else if let Some(gb_str) = rest.strip_suffix(" GB") {
248 current_size_mb = gb_str.trim().parse::<u64>().ok().map(|g| g * 1024);
249 } else if let Some(mb_str) = rest.strip_suffix(" MB") {
250 current_size_mb = mb_str.trim().parse::<u64>().ok();
251 }
252 } else if let Some(rest) = trimmed.strip_prefix("Type:") {
253 let t = rest.trim();
254 if t != "Unknown" && !t.is_empty() {
255 current_type = t.to_string();
256 }
257 } else if let Some(rest) = trimmed.strip_prefix("Speed:") {
258 let rest = rest.trim();
259 if let Some(mt_str) = rest.strip_suffix(" MT/s") {
260 current_speed = mt_str.trim().parse::<u64>().ok();
261 } else if let Some(mhz_str) = rest.strip_suffix(" MHz") {
262 current_speed = mhz_str.trim().parse::<u64>().ok().map(|mhz| mhz * 2);
264 }
265 }
266 }
267
268 if let Some(mb) = current_size_mb {
270 if mb > 0 {
271 slots.push(DimmSlot {
272 size_mb: mb,
273 mem_type: current_type,
274 speed_mt: current_speed,
275 });
276 }
277 }
278
279 format_dimm_slots(&slots)
283}
284
285#[cfg(target_os = "windows")]
286fn detect_windows() -> Option<String> {
287 let cmd = "Get-CimInstance Win32_PhysicalMemory | \
288 Select-Object Capacity,SMBIOSMemoryType,Speed | \
289 ConvertTo-Csv -NoTypeInformation";
290 if let Ok(output) = std::process::Command::new("powershell")
291 .args(["-NoProfile", "-NonInteractive", "-Command", cmd])
292 .output()
293 {
294 if output.status.success() {
295 let text = String::from_utf8_lossy(&output.stdout);
296 if let Some(result) = parse_win32_physical_memory(&text) {
297 return Some(result);
298 }
299 }
300 }
301
302 let fb_cmd = "(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory";
305 let Ok(fb) = std::process::Command::new("powershell")
306 .args(["-NoProfile", "-NonInteractive", "-Command", fb_cmd])
307 .output()
308 else {
309 return None;
310 };
311 if !fb.status.success() {
312 return None;
313 }
314 let total_bytes: u64 = String::from_utf8_lossy(&fb.stdout).trim().parse().ok()?;
315 if total_bytes == 0 {
316 return None;
317 }
318 let gb = total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
319 Some(format!("{:.0} GB (VM — DIMM info unavailable)", gb))
320}
321
322#[cfg(target_os = "windows")]
327pub fn parse_win32_physical_memory(csv: &str) -> Option<String> {
328 let mut lines = csv.lines();
329 lines.next(); let mut slots = Vec::new();
331 for line in lines {
332 let fields = unquoted_csv_fields(line);
333 if fields.len() < 3 {
334 continue;
335 }
336 let capacity_bytes: u64 = fields[0].trim().parse().unwrap_or(0);
337 if capacity_bytes == 0 {
338 continue;
339 }
340 let size_mb = capacity_bytes / (1024 * 1024);
341 let type_code: u16 = fields[1].trim().parse().unwrap_or(0);
342 let speed_mt: u64 = fields[2].trim().parse().unwrap_or(0);
343 slots.push(DimmSlot {
344 size_mb,
345 mem_type: smbios_memory_type(type_code),
346 speed_mt: if speed_mt > 0 { Some(speed_mt) } else { None },
347 });
348 }
349 format_dimm_slots(&slots)
350}
351
352#[cfg(target_os = "windows")]
354fn smbios_memory_type(code: u16) -> String {
355 match code {
356 20 => "DDR".to_string(),
357 21 => "DDR2".to_string(),
358 24 => "DDR3".to_string(),
359 26 => "DDR4".to_string(),
360 27 => "LPDDR".to_string(),
361 28 => "LPDDR2".to_string(),
362 29 => "LPDDR3".to_string(),
363 30 => "LPDDR4".to_string(),
364 34 => "DDR5".to_string(),
365 35 => "LPDDR5".to_string(),
366 _ => String::new(),
367 }
368}
369
370#[cfg(target_os = "windows")]
374fn unquoted_csv_fields(line: &str) -> Vec<String> {
375 let line = line.trim().trim_matches('"');
376 line.split("\",\"").map(|s| s.to_string()).collect()
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[cfg(target_os = "linux")]
384 #[test]
385 fn test_parse_dmidecode_two_slots() {
386 let input = r#"
387Memory Device
388 Size: 8 GB
389 Type: DDR5
390 Speed: 4800 MT/s
391
392Memory Device
393 Size: 8 GB
394 Type: DDR5
395 Speed: 4800 MT/s
396"#;
397 let slots = parse_dmidecode_type17(input);
398 assert_eq!(slots.len(), 2);
399 assert_eq!(slots[0].size_mb, 8192);
400 assert_eq!(slots[0].mem_type, "DDR5");
401 assert_eq!(slots[0].speed_mt, Some(4800));
402
403 let summary = format_dimm_slots(&slots).unwrap();
404 assert_eq!(summary, "2× 8 GB DDR5 4800 MT/s");
405 }
406
407 #[cfg(target_os = "linux")]
408 #[test]
409 fn test_parse_dmidecode_gib_units() {
410 let input = r#"
412Memory Device
413 Size: 2 GiB
414 Type: LPDDR5
415 Speed: 6400 MT/s
416
417Memory Device
418 Size: 2 GiB
419 Type: LPDDR5
420 Speed: 6400 MT/s
421"#;
422 let slots = parse_dmidecode_type17(input);
423 assert_eq!(slots.len(), 2);
424 assert_eq!(slots[0].size_mb, 2048);
425 assert_eq!(slots[0].mem_type, "LPDDR5");
426 assert_eq!(slots[0].speed_mt, Some(6400));
427
428 let summary = format_dimm_slots(&slots).unwrap();
429 assert_eq!(summary, "2× 2 GB LPDDR5 6400 MT/s");
430 }
431
432 #[cfg(target_os = "linux")]
433 #[test]
434 fn test_parse_dmidecode_empty_slot() {
435 let input = r#"
436Memory Device
437 Size: No Module Installed
438 Type: Unknown
439
440Memory Device
441 Size: 16 GB
442 Type: DDR4
443 Speed: 3200 MT/s
444"#;
445 let slots = parse_dmidecode_type17(input);
446 assert_eq!(slots.len(), 1);
447 assert_eq!(slots[0].size_mb, 16384);
448 let summary = format_dimm_slots(&slots).unwrap();
449 assert_eq!(summary, "16 GB DDR4 3200 MT/s");
450 }
451
452 #[cfg(target_os = "linux")]
453 #[test]
454 fn test_parse_dmidecode_mixed_sizes() {
455 let input = r#"
456Memory Device
457 Size: 16 GB
458 Type: DDR5
459 Speed: 5600 MT/s
460
461Memory Device
462 Size: 32 GB
463 Type: DDR5
464 Speed: 5600 MT/s
465"#;
466 let slots = parse_dmidecode_type17(input);
467 assert_eq!(slots.len(), 2);
468 let summary = format_dimm_slots(&slots).unwrap();
469 assert!(summary.contains("16 GB DDR5 5600 MT/s"));
471 assert!(summary.contains("32 GB DDR5 5600 MT/s"));
472 }
473
474 #[cfg(target_os = "macos")]
475 #[test]
476 fn test_parse_system_profiler_apple_silicon() {
477 let input = r#"
478Memory:
479
480 Type: LPDDR5
481 Speed: 6400 MT/s
482 Size: 16 GB
483"#;
484 let result = parse_system_profiler_memory(input);
485 assert_eq!(result, Some("16 GB LPDDR5 6400 MT/s".to_string()));
486 }
487
488 #[cfg(target_os = "macos")]
489 #[test]
490 fn test_parse_system_profiler_multi_slot() {
491 let input = r#"
492Memory:
493
494 BANK 0/DIMM0:
495
496 Size: 16 GB
497 Type: DDR5
498 Speed: 4800 MT/s
499
500 BANK 1/DIMM0:
501
502 Size: 16 GB
503 Type: DDR5
504 Speed: 4800 MT/s
505"#;
506 let result = parse_system_profiler_memory(input);
507 assert_eq!(result, Some("2× 16 GB DDR5 4800 MT/s".to_string()));
508 }
509
510 #[cfg(target_os = "windows")]
511 #[test]
512 fn test_parse_win32_physical_memory_ddr4() {
513 let csv = r#""Capacity","SMBIOSMemoryType","Speed"
514"8589934592","26","3200"
515"8589934592","26","3200"
516"#;
517 let result = parse_win32_physical_memory(csv);
518 assert_eq!(result, Some("2× 8 GB DDR4 3200 MT/s".to_string()));
519 }
520
521 #[cfg(target_os = "windows")]
522 #[test]
523 fn test_parse_win32_physical_memory_ddr5() {
524 let csv = r#""Capacity","SMBIOSMemoryType","Speed"
525"17179869184","34","4800"
526"17179869184","34","4800"
527"#;
528 let result = parse_win32_physical_memory(csv);
529 assert_eq!(result, Some("2× 16 GB DDR5 4800 MT/s".to_string()));
530 }
531
532 #[cfg(target_os = "windows")]
533 #[test]
534 fn test_parse_win32_physical_memory_unknown_type() {
535 let csv = r#""Capacity","SMBIOSMemoryType","Speed"
537"34359738368","0","0"
538"#;
539 let result = parse_win32_physical_memory(csv);
540 assert_eq!(result, Some("32 GB".to_string()));
541 }
542
543 #[cfg(target_os = "windows")]
544 #[test]
545 fn test_parse_win32_physical_memory_mixed_slots() {
546 let csv = r#""Capacity","SMBIOSMemoryType","Speed"
547"8589934592","26","3200"
548"16777216","26","3200"
549"#;
550 let result = parse_win32_physical_memory(csv);
552 let s = result.unwrap();
553 assert!(s.contains("8 GB DDR4 3200 MT/s"));
554 assert!(s.contains("16 MB DDR4 3200 MT/s"));
555 }
556}