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(not(any(target_os = "linux", target_os = "macos")))]
14 return None;
15}
16
17#[cfg(any(target_os = "linux", target_os = "macos"))]
19#[derive(Debug, PartialEq)]
20pub struct DimmSlot {
21 pub size_mb: u64,
22 pub mem_type: String,
23 pub speed_mt: Option<u64>,
24}
25
26#[cfg(target_os = "linux")]
28pub fn parse_dmidecode_type17(output: &str) -> Vec<DimmSlot> {
29 let mut slots = Vec::new();
30 let mut size_mb: Option<u64> = None;
31 let mut mem_type = String::new();
32 let mut speed_mt: Option<u64> = None;
33
34 for line in output.lines() {
35 let trimmed = line.trim();
36
37 if trimmed == "Memory Device" {
38 if let Some(mb) = size_mb.take() {
40 if mb > 0 {
41 slots.push(DimmSlot {
42 size_mb: mb,
43 mem_type: mem_type.clone(),
44 speed_mt,
45 });
46 }
47 }
48 mem_type.clear();
49 speed_mt = None;
50 } else if let Some(rest) = trimmed.strip_prefix("Size:") {
51 let rest = rest.trim();
52 if rest.contains("No Module") || rest == "Unknown" {
53 size_mb = Some(0);
54 } else if let Some(n) = rest
55 .strip_suffix(" GiB")
56 .or_else(|| rest.strip_suffix(" GB"))
57 {
58 size_mb = n.trim().parse::<u64>().ok().map(|g| g * 1024);
59 } else if let Some(n) = rest
60 .strip_suffix(" MiB")
61 .or_else(|| rest.strip_suffix(" MB"))
62 {
63 size_mb = n.trim().parse::<u64>().ok();
64 }
65 } else if let Some(rest) = trimmed.strip_prefix("Type:") {
66 let t = rest.trim();
67 if t != "Unknown" && !t.is_empty() {
68 mem_type = t.to_string();
69 }
70 } else if let Some(rest) = trimmed.strip_prefix("Speed:") {
71 let rest = rest.trim();
73 if let Some(mt_str) = rest.strip_suffix(" MT/s") {
74 speed_mt = mt_str.trim().parse::<u64>().ok();
75 }
76 }
77 }
78
79 if let Some(mb) = size_mb {
81 if mb > 0 {
82 slots.push(DimmSlot {
83 size_mb: mb,
84 mem_type,
85 speed_mt,
86 });
87 }
88 }
89
90 slots
91}
92
93#[cfg(any(target_os = "linux", target_os = "macos"))]
95pub fn format_dimm_slots(slots: &[DimmSlot]) -> Option<String> {
96 if slots.is_empty() {
97 return None;
98 }
99
100 #[derive(PartialEq, Eq, Hash)]
102 struct Key {
103 size_mb: u64,
104 mem_type: String,
105 speed_mt: Option<u64>,
106 }
107
108 let mut groups: Vec<(Key, usize)> = Vec::new();
109 for slot in slots {
110 let key = Key {
111 size_mb: slot.size_mb,
112 mem_type: slot.mem_type.clone(),
113 speed_mt: slot.speed_mt,
114 };
115 if let Some(entry) = groups.iter_mut().find(|(k, _)| k == &key) {
116 entry.1 += 1;
117 } else {
118 groups.push((key, 1));
119 }
120 }
121
122 let parts: Vec<String> = groups
123 .iter()
124 .map(|(key, count)| {
125 let size_str = if key.size_mb >= 1024 {
126 format!("{} GB", key.size_mb / 1024)
127 } else {
128 format!("{} MB", key.size_mb)
129 };
130
131 let mut s = if *count > 1 {
132 format!("{}× {}", count, size_str)
133 } else {
134 size_str
135 };
136
137 if !key.mem_type.is_empty() {
138 s.push(' ');
139 s.push_str(&key.mem_type);
140 }
141
142 if let Some(mt) = key.speed_mt {
143 s.push_str(&format!(" {} MT/s", mt));
144 }
145
146 s
147 })
148 .collect();
149
150 Some(parts.join(", "))
151}
152
153#[cfg(target_os = "linux")]
154fn detect_linux() -> Option<String> {
155 let candidates = ["dmidecode", "/usr/bin/dmidecode", "/usr/sbin/dmidecode"];
157 for cmd in candidates {
158 let Ok(output) = std::process::Command::new(cmd)
159 .args(["--type", "17"])
160 .output()
161 else {
162 continue;
163 };
164 if !output.status.success() {
165 continue;
166 }
167 let text = String::from_utf8_lossy(&output.stdout);
168 let slots = parse_dmidecode_type17(&text);
169 return format_dimm_slots(&slots);
170 }
171 None
172}
173
174#[cfg(target_os = "macos")]
175fn detect_macos() -> Option<String> {
176 let output = std::process::Command::new("system_profiler")
177 .args(["SPMemoryDataType", "-detailLevel", "basic"])
178 .output()
179 .ok()?;
180
181 if !output.status.success() {
182 return None;
183 }
184
185 let text = String::from_utf8_lossy(&output.stdout);
186 parse_system_profiler_memory(&text)
187}
188
189#[cfg(target_os = "macos")]
191pub fn parse_system_profiler_memory(text: &str) -> Option<String> {
192 let mut slots: Vec<DimmSlot> = Vec::new();
205 let mut current_size_mb: Option<u64> = None;
206 let mut current_type = String::new();
207 let mut current_speed: Option<u64> = None;
208 let mut in_slot = false;
209
210 for line in text.lines() {
211 let trimmed = line.trim();
212
213 if (trimmed.contains("DIMM") || trimmed.contains("BANK") || trimmed.contains("Slot"))
215 && trimmed.ends_with(':')
216 {
217 if in_slot {
218 if let Some(mb) = current_size_mb.take() {
219 if mb > 0 {
220 slots.push(DimmSlot {
221 size_mb: mb,
222 mem_type: current_type.clone(),
223 speed_mt: current_speed,
224 });
225 }
226 }
227 current_type.clear();
228 current_speed = None;
229 }
230 in_slot = true;
231 continue;
232 }
233
234 let size_rest = trimmed.strip_prefix("Size:").or_else(|| {
236 trimmed
237 .strip_prefix("Memory:")
238 .filter(|s| !s.trim().is_empty())
239 });
240 if let Some(rest) = size_rest {
241 let rest = rest.trim();
242 if rest.contains("Empty") || rest == "Unknown" {
243 current_size_mb = Some(0);
244 } else if let Some(gb_str) = rest.strip_suffix(" GB") {
245 current_size_mb = gb_str.trim().parse::<u64>().ok().map(|g| g * 1024);
246 } else if let Some(mb_str) = rest.strip_suffix(" MB") {
247 current_size_mb = mb_str.trim().parse::<u64>().ok();
248 }
249 } else if let Some(rest) = trimmed.strip_prefix("Type:") {
250 let t = rest.trim();
251 if t != "Unknown" && !t.is_empty() {
252 current_type = t.to_string();
253 }
254 } else if let Some(rest) = trimmed.strip_prefix("Speed:") {
255 let rest = rest.trim();
256 if let Some(mt_str) = rest.strip_suffix(" MT/s") {
257 current_speed = mt_str.trim().parse::<u64>().ok();
258 } else if let Some(mhz_str) = rest.strip_suffix(" MHz") {
259 current_speed = mhz_str.trim().parse::<u64>().ok().map(|mhz| mhz * 2);
261 }
262 }
263 }
264
265 if let Some(mb) = current_size_mb {
267 if mb > 0 {
268 slots.push(DimmSlot {
269 size_mb: mb,
270 mem_type: current_type,
271 speed_mt: current_speed,
272 });
273 }
274 }
275
276 format_dimm_slots(&slots)
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[cfg(target_os = "linux")]
287 #[test]
288 fn test_parse_dmidecode_two_slots() {
289 let input = r#"
290Memory Device
291 Size: 8 GB
292 Type: DDR5
293 Speed: 4800 MT/s
294
295Memory Device
296 Size: 8 GB
297 Type: DDR5
298 Speed: 4800 MT/s
299"#;
300 let slots = parse_dmidecode_type17(input);
301 assert_eq!(slots.len(), 2);
302 assert_eq!(slots[0].size_mb, 8192);
303 assert_eq!(slots[0].mem_type, "DDR5");
304 assert_eq!(slots[0].speed_mt, Some(4800));
305
306 let summary = format_dimm_slots(&slots).unwrap();
307 assert_eq!(summary, "2× 8 GB DDR5 4800 MT/s");
308 }
309
310 #[cfg(target_os = "linux")]
311 #[test]
312 fn test_parse_dmidecode_gib_units() {
313 let input = r#"
315Memory Device
316 Size: 2 GiB
317 Type: LPDDR5
318 Speed: 6400 MT/s
319
320Memory Device
321 Size: 2 GiB
322 Type: LPDDR5
323 Speed: 6400 MT/s
324"#;
325 let slots = parse_dmidecode_type17(input);
326 assert_eq!(slots.len(), 2);
327 assert_eq!(slots[0].size_mb, 2048);
328 assert_eq!(slots[0].mem_type, "LPDDR5");
329 assert_eq!(slots[0].speed_mt, Some(6400));
330
331 let summary = format_dimm_slots(&slots).unwrap();
332 assert_eq!(summary, "2× 2 GB LPDDR5 6400 MT/s");
333 }
334
335 #[cfg(target_os = "linux")]
336 #[test]
337 fn test_parse_dmidecode_empty_slot() {
338 let input = r#"
339Memory Device
340 Size: No Module Installed
341 Type: Unknown
342
343Memory Device
344 Size: 16 GB
345 Type: DDR4
346 Speed: 3200 MT/s
347"#;
348 let slots = parse_dmidecode_type17(input);
349 assert_eq!(slots.len(), 1);
350 assert_eq!(slots[0].size_mb, 16384);
351 let summary = format_dimm_slots(&slots).unwrap();
352 assert_eq!(summary, "16 GB DDR4 3200 MT/s");
353 }
354
355 #[cfg(target_os = "linux")]
356 #[test]
357 fn test_parse_dmidecode_mixed_sizes() {
358 let input = r#"
359Memory Device
360 Size: 16 GB
361 Type: DDR5
362 Speed: 5600 MT/s
363
364Memory Device
365 Size: 32 GB
366 Type: DDR5
367 Speed: 5600 MT/s
368"#;
369 let slots = parse_dmidecode_type17(input);
370 assert_eq!(slots.len(), 2);
371 let summary = format_dimm_slots(&slots).unwrap();
372 assert!(summary.contains("16 GB DDR5 5600 MT/s"));
374 assert!(summary.contains("32 GB DDR5 5600 MT/s"));
375 }
376
377 #[cfg(target_os = "macos")]
378 #[test]
379 fn test_parse_system_profiler_apple_silicon() {
380 let input = r#"
381Memory:
382
383 Type: LPDDR5
384 Speed: 6400 MT/s
385 Size: 16 GB
386"#;
387 let result = parse_system_profiler_memory(input);
388 assert_eq!(result, Some("16 GB LPDDR5 6400 MT/s".to_string()));
389 }
390
391 #[cfg(target_os = "macos")]
392 #[test]
393 fn test_parse_system_profiler_multi_slot() {
394 let input = r#"
395Memory:
396
397 BANK 0/DIMM0:
398
399 Size: 16 GB
400 Type: DDR5
401 Speed: 4800 MT/s
402
403 BANK 1/DIMM0:
404
405 Size: 16 GB
406 Type: DDR5
407 Speed: 4800 MT/s
408"#;
409 let result = parse_system_profiler_memory(input);
410 assert_eq!(result, Some("2× 16 GB DDR5 4800 MT/s".to_string()));
411 }
412}