CH-J ToolsSSH-এর মাধ্যমে সার্ভার ব্যবস্থাপনা
মেনু
প্রকাশিত সোর্স

CH-J Tools

নির্দিষ্ট একটি অ্যাপ্লিকেশন রিলিজের ডিরেক্টরি ও ফাইল ব্রাউজ করুন।

সোর্স ZIP ডাউনলোড করুন
CH-J / Server Manager 1704 টি ফাইল
0.1.2-beta যোগ করা হয়েছে: · বিল্ড হয়েছে:
inventory.rs যোগ করা হয়েছে: তারিখ নথিভুক্ত নেই ৫১.৮ KB · 1074 লাইন
কোড ফাইল ডাউনলোড করুন
1//! chj.disks model from the output of `remoteDisks.sh inspect` (port of `disks/inventory.js`).
2//!
3//! The device graph comes from sysfs (holders/slaves), so layered stacks such as
4//! disk → partition → LUKS → LVM PV → VG → LV → ext4 are represented without assuming that a
5//! partition directly contains a file system. lsblk/blkid/sfdisk/LVM JSON only enrich nodes;
6//! without them (BusyBox, no root) the model is marked partial instead of guessing.
8use super::js::{self, int_opt, int_str, lines, num, number, number_str, text, text_or_null, truthy};
9use once_cell::sync::Lazy;
10use regex::Regex;
11use serde_json::{json, Map, Value};
12use std::collections::{BTreeMap, HashMap};
14pub const MIB: f64 = 1_048_576.0;
16pub fn gpt_type(code: &str) -> Option<&'static str> {
17 Some(match code {
18 "0FC63DAF-8483-4772-8E79-3D69D8477DE4" => "linux",
19 "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709" => "linux-root-x86-64",
20 "B921B045-1DF0-41C3-AF44-4C6F280D3FAE" => "linux-root-arm64",
21 "BC13C2FF-59E6-4262-A352-B275FD6F7172" => "linux-extended-boot",
22 "933AC7E1-2EB4-4F13-B844-0E14E2AEF915" => "linux-home",
23 "3B8F8425-20E0-4F3B-907F-1A25A76F98E8" => "linux-server-data",
24 "4D21B016-B534-45C2-A9FB-5C16E091FD2D" => "linux-var",
25 "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F" => "swap",
26 "E6D6D379-F507-44C2-A23C-238F2A3DF928" => "lvm",
27 "A19D880F-05FC-4D3B-A006-743F0F84911E" => "raid",
28 "CA7D7CCB-63ED-4C53-861C-1742536059CC" => "luks",
29 "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" => "esp",
30 "21686148-6449-6E6F-744E-656564454649" => "bios-boot",
31 "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" => "basic-data",
32 "E3C9E316-0B5C-4DB8-817D-F92DF00215AE" => "microsoft-reserved",
33 "DE94BBA4-06D1-4D40-A16A-BFD50179D6AC" => "windows-recovery",
34 "6A898CC3-1DD2-11B2-99A6-080020736631" => "zfs",
35 "516E7CBA-6ECF-11D6-8FF8-00022D09712B" => "freebsd-zfs",
36 _ => return None,
37 })
40pub fn dos_type(code: &str) -> Option<&'static str> {
41 Some(match code {
42 "83" => "linux",
43 "82" => "swap",
44 "8e" => "lvm",
45 "fd" => "raid",
46 "ef" => "esp",
47 "7" => "basic-data",
48 "b" | "c" => "fat32",
49 "e" => "fat16",
50 "5" | "f" | "85" => "extended",
51 "ee" => "gpt-protective",
52 _ => return None,
53 })
56/// Partition types CH-J writes (GPT GUID / MBR type) for new partitions and type flags.
57pub fn partition_type_code(flag: &str, table: &str) -> Option<&'static str> {
58 let (gpt, dos) = match flag {
59 "linux" => ("0FC63DAF-8483-4772-8E79-3D69D8477DE4", Some("83")),
60 "swap" => ("0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", Some("82")),
61 "lvm" => ("E6D6D379-F507-44C2-A23C-238F2A3DF928", Some("8e")),
62 "raid" => ("A19D880F-05FC-4D3B-A006-743F0F84911E", Some("fd")),
63 "esp" => ("C12A7328-F81F-11D2-BA4B-00A0C93EC93B", Some("ef")),
64 "bios-boot" => ("21686148-6449-6E6F-744E-656564454649", None),
65 "basic-data" => ("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", Some("7")),
66 "fat32" => ("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", Some("c")),
67 _ => return None,
68 };
69 match table {
70 "gpt" => Some(gpt),
71 "dos" => dos,
72 _ => None,
73 }
76pub static SYSTEM_MOUNTS: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(/|/boot(/.*)?|/efi|/usr(/.*)?|/var(/.*)?|/home|/opt|/srv|/root|/tmp|/nix|/snap(/.*)?)$").unwrap());
77static VIRTUAL_MODEL: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)QEMU|VBOX|VirtualBox|VMware|Virtual disk|Virtual HD|Msft Virtual|Xen|Hyper-V|BHYVE|Red Hat VirtIO").unwrap());
78static SECTION: Lazy<Regex> = Lazy::new(|| Regex::new(r"\x1eCHJ_SECTION=([a-z]+)\r?\n").unwrap());
80pub fn split_sections(stdout: &str) -> HashMap<String, String> {
81 let mut sections = HashMap::new();
82 let mut current: Option<String> = None;
83 let mut start = 0;
84 for caps in SECTION.captures_iter(stdout) {
85 let whole = caps.get(0).unwrap();
86 if let Some(name) = current.take() {
87 sections.insert(name, stdout[start..whole.start()].to_string());
88 }
89 current = Some(caps[1].to_string());
90 start = whole.end();
91 }
92 if let Some(name) = current {
93 sections.insert(name, stdout[start..].to_string());
94 }
95 sections
98/// 0.1.0 inventory `keyValues` (a line without "=" yields key = line minus its last character).
99pub fn key_values(value: &str) -> BTreeMap<String, String> {
100 let mut out = BTreeMap::new();
101 for line in lines(value) {
102 let units: Vec<u16> = line.encode_utf16().collect();
103 let (key, val) = match line.find('=') {
104 Some(i) => (line[..i].to_string(), line[i + 1..].to_string()),
105 None => (String::from_utf16_lossy(&units[..units.len().saturating_sub(1)]), line.to_string()),
106 };
107 if !key.is_empty() {
108 out.insert(key, val);
109 }
110 }
111 out
114fn parse_json(value: &str) -> Value {
115 serde_json::from_str(value).unwrap_or(Value::Null)
118static OCTAL: Lazy<Regex> = Lazy::new(|| Regex::new(r"\\(\d{3})").unwrap());
120pub fn unescape_mount(value: &str) -> String {
121 OCTAL
122 .replace_all(value, |c: &regex::Captures| {
123 // parseInt("089", 8) stops at the first invalid digit.
124 let digits: String = c[1].chars().take_while(|d| ('0'..='7').contains(d)).collect();
125 match u32::from_str_radix(&digits, 8).ok().and_then(char::from_u32) {
126 Some(ch) => ch.to_string(),
127 None => "\u{0}".to_string(),
128 }
129 })
130 .to_string()
133#[derive(Debug, Clone)]
134pub struct SysEntry {
135 pub kname: String,
136 pub devno: String,
137 pub sectors512: Value,
138 pub read_only: bool,
139 pub removable: bool,
140 pub partition: Value,
141 pub start512: Value,
142 pub parent: Value,
143 pub rotational: Value,
144 pub logical_sector_size: f64,
145 pub physical_sector_size: f64,
146 pub model: String,
147 pub vendor: String,
148 pub serial: String,
149 pub wwid: String,
150 pub holders: Vec<String>,
151 pub slaves: Vec<String>,
152 pub dm_name: String,
153 pub dm_uuid: String,
154 pub loop_backing: String,
155 pub md_level: String,
156 pub sys_path: String,
157 pub discard_max: Value,
160static KNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[A-Za-z0-9._:+-]{1,64}$").unwrap());
162fn parse_sysblock(section: &str) -> Vec<SysEntry> {
163 lines(section)
164 .into_iter()
165 .map(|line| {
166 let f: Vec<&str> = line.split('|').collect();
167 let field = |i: usize| f.get(i).copied();
168 let get = |i: usize| field(i).unwrap_or("");
169 let list = |i: usize| get(i).split(',').filter(|s| !s.is_empty()).map(|n| text(n, 64)).collect::<Vec<_>>();
170 let or_default = |v: Value, d: f64| if truthy(&v) { number(&v) } else { d };
171 SysEntry {
172 kname: text(get(0), 64),
173 devno: text(get(1), 16),
174 sectors512: int_str(field(2)),
175 read_only: field(3) == Some("1"),
176 removable: field(4) == Some("1"),
177 partition: if field(5) == Some("-") { Value::Null } else { int_str(field(5)) },
178 start512: if field(6) == Some("-") { Value::Null } else { int_str(field(6)) },
179 parent: if field(7) == Some("-") { Value::Null } else { json!(text(get(7), 64)) },
180 rotational: if field(8) == Some("") { Value::Null } else { json!(field(8) == Some("1")) },
181 logical_sector_size: or_default(int_str(field(9)), 512.0),
182 physical_sector_size: or_default(int_str(field(10)), 512.0),
183 model: text(get(11), 80),
184 vendor: text(get(12), 40),
185 serial: text(get(13), 80),
186 wwid: text(get(14), 120),
187 holders: list(15),
188 slaves: list(16),
189 dm_name: text(get(17), 128),
190 dm_uuid: text(get(18), 160),
191 loop_backing: text(get(19), 200),
192 md_level: text(get(20), 16),
193 sys_path: text(get(21), 200),
194 discard_max: int_str(field(22)),
195 }
196 })
197 .filter(|e| KNAME.is_match(&e.kname))
198 .collect()
201static BLANK_LINE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\n\s*\n").unwrap());
203fn parse_blkid(section: &str) -> HashMap<String, BTreeMap<String, String>> {
204 let mut result = HashMap::new();
205 for block in BLANK_LINE.split(section) {
206 let values = key_values(block);
207 if let Some(dev) = values.get("DEVNAME").filter(|d| !d.is_empty()).cloned() {
208 result.insert(dev, values);
209 }
210 }
211 result
214struct Mount {
215 devno: String,
216 mountpoint: String,
217 options: String,
218 fstype: String,
219 source: String,
222fn parse_mountinfo(section: &str) -> Vec<Mount> {
223 lines(section)
224 .into_iter()
225 .map(|line| {
226 let mut halves = line.split(" - ");
227 let left = halves.next().unwrap_or("");
228 let right = halves.next().unwrap_or("");
229 let fields: Vec<&str> = left.split(' ').collect();
230 let tail: Vec<&str> = right.split(' ').collect();
231 Mount {
232 devno: fields.get(2).copied().unwrap_or("").to_string(),
233 mountpoint: unescape_mount(fields.get(4).copied().unwrap_or("")),
234 options: fields.get(5).copied().unwrap_or("").to_string(),
235 fstype: tail.first().copied().unwrap_or("").to_string(),
236 source: unescape_mount(tail.get(1).copied().unwrap_or("")),
237 }
238 })
239 .filter(|m| !m.mountpoint.is_empty())
240 .collect()
243static DF: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.+)$").unwrap());
245fn parse_df(section: &str) -> HashMap<String, Value> {
246 let mut result = HashMap::new();
247 for line in lines(section).into_iter().skip(1) {
248 if let Some(c) = DF.captures(line) {
249 let k = |i: usize| num(number_str(&c[i]) * 1024.0);
250 result.insert(c[6].to_string(), json!({ "size": k(2), "used": k(3), "available": k(4) }));
251 }
252 }
253 result
256struct Swap {
257 path: String,
258 used: Value,
261fn parse_swaps(section: &str) -> Vec<Swap> {
262 lines(section)
263 .into_iter()
264 .skip(1)
265 .map(|line| {
266 let f: Vec<&str> = line.split_whitespace().collect();
267 Swap { path: unescape_mount(f.first().copied().unwrap_or("undefined")), used: num(f.get(3).map(|v| number_str(v)).unwrap_or(f64::NAN) * 1024.0) }
268 })
269 .collect()
272static PT: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?s)@@disk (\S+) (\S+)\n(.*?)\n@@end").unwrap());
273static HASH64: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-f0-9]{64}$").unwrap());
275fn parse_partition_tables(section: &str) -> HashMap<String, Value> {
276 let mut tables = HashMap::new();
277 for c in PT.captures_iter(section) {
278 let json = parse_json(&c[3]);
279 if let Some(Value::Object(pt)) = json.get("partitiontable") {
280 let mut table = pt.clone();
281 table.insert("hash".into(), if HASH64.is_match(&c[2]) { json!(&c[2]) } else { Value::Null });
282 tables.insert(c[1].to_string(), Value::Object(table));
283 }
284 }
285 tables
288/// `split(/^@@<prefix>/m).filter(Boolean)`.
289fn split_marker<'a>(section: &'a str, marker: &str) -> Vec<&'a str> {
290 let mut parts = Vec::new();
291 let mut start = 0;
292 let mut search = 0;
293 while let Some(found) = section[search..].find(marker) {
294 let at = search + found;
295 if at == 0 || section.as_bytes()[at - 1] == b'\n' {
296 parts.push(&section[start..at]);
297 start = at + marker.len();
298 }
299 search = at + marker.len();
300 }
301 parts.push(&section[start..]);
302 parts.into_iter().filter(|p| !p.is_empty()).collect()
305/// `block.slice(0, block.indexOf("\n"))` / `block.slice(block.indexOf("\n") + 1)` (no newline → drop last char / whole block).
306fn head_body(block: &str) -> (String, &str) {
307 match block.find('\n') {
308 Some(i) => (block[..i].to_string(), &block[i + 1..]),
309 None => {
310 let units: Vec<u16> = block.encode_utf16().collect();
311 (String::from_utf16_lossy(&units[..units.len().saturating_sub(1)]), block)
312 }
313 }
316static PVE_GUEST: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(?:vm|base)-(\d+)-(?:disk|cloudinit|state|tpmstate)").unwrap());
318fn parse_lvm(section: Option<&str>) -> Value {
319 let section = section.unwrap_or("");
320 let mut parts: HashMap<String, Value> = HashMap::new();
321 for block in split_marker(section, "@@") {
322 let (name, body) = head_body(block);
323 let report = parse_json(body).get("report").and_then(|r| r.get(0)).cloned().filter(truthy).unwrap_or_else(|| json!({}));
324 parts.insert(name.trim().to_string(), report);
325 }
326 let n = |value: &Value| {
327 let s = js::string(Some(value)).trim().to_string();
328 int_opt(Some(&json!(s)))
329 };
330 let t = |value: &Value, limit: usize| json!(text(&js::string(Some(value)), limit));
331 let list = |name: &str, key: &str| parts.get(name).and_then(|p| p.get(key)).and_then(Value::as_array).cloned().unwrap_or_default();
332 let pvs: Vec<Value> = list("pvs", "pv")
333 .iter()
334 .map(|pv| json!({ "path": t(&pv["pv_name"], 200), "vg": t(&pv["vg_name"], 128), "size": n(&pv["pv_size"]), "free": n(&pv["pv_free"]), "deviceSize": n(&pv["dev_size"]), "uuid": t(&pv["pv_uuid"], 64), "attr": t(&pv["pv_attr"], 16) }))
335 .collect();
336 let vgs: Vec<Value> = list("vgs", "vg")
337 .iter()
338 .map(|vg| {
339 json!({ "name": t(&vg["vg_name"], 128), "uuid": t(&vg["vg_uuid"], 64), "size": n(&vg["vg_size"]), "free": n(&vg["vg_free"]), "extentSize": n(&vg["vg_extent_size"]), "freeExtents": n(&vg["vg_free_count"]), "pvCount": n(&vg["pv_count"]), "lvCount": n(&vg["lv_count"]), "attr": t(&vg["vg_attr"], 16) })
340 })
341 .collect();
342 let lvs: Vec<Value> = list("lvs", "lv")
343 .iter()
344 .filter(|lv| !js::string(Some(&lv["lv_name"])).starts_with('['))
345 .map(|lv| {
346 let attr = text(&js::string(Some(&lv["lv_attr"])), 16);
347 let a: Vec<char> = attr.chars().collect();
348 let name = js::string(Some(&lv["lv_name"]));
349 json!({
350 "name": t(&lv["lv_name"], 128), "vg": t(&lv["vg_name"], 128), "uuid": t(&lv["lv_uuid"], 64), "size": n(&lv["lv_size"]), "attr": attr,
351 "path": t(&lv["lv_path"], 200), "dmPath": t(&lv["lv_dm_path"], 200), "pool": t(&lv["pool_lv"], 128), "dataPercent": n(&lv["data_percent"]),
352 "metadataPercent": n(&lv["metadata_percent"]), "segtype": t(&lv["segtype"], 32), "origin": t(&lv["origin"], 128),
353 "thinPool": a.first() == Some(&'t'), "thinVolume": a.first() == Some(&'V'), "snapshot": matches!(a.first(), Some('s') | Some('S')), "active": a.get(4) == Some(&'a'),
354 "pveGuest": PVE_GUEST.captures(&name).map(|c| json!(&c[1])).unwrap_or(Value::Null)
355 })
356 })
357 .collect();
358 json!({ "available": !section.is_empty(), "pvs": pvs, "vgs": vgs, "lvs": lvs })
361fn parse_crypt(section: &str) -> HashMap<String, Value> {
362 let mut result = HashMap::new();
363 for block in split_marker(section, "@@map ") {
364 let (name, body) = head_body(block);
365 let mut values: HashMap<String, String> = HashMap::new();
366 for line in lines(body) {
367 if let Some(i) = line.find(':').filter(|i| *i > 0) {
368 values.insert(line[..i].trim().to_lowercase(), text(&line[i + 1..], 120));
369 }
370 }
371 let get = |k: &str| values.get(k).filter(|v| !v.is_empty()).map(|v| json!(v)).unwrap_or(Value::Null);
372 result.insert(
373 name.trim().to_string(),
374 json!({ "type": get("type"), "cipher": get("cipher"), "keySize": get("keysize"), "keyLocation": get("key location"), "device": get("device"), "mode": get("mode"), "sectorSize": int_str(values.get("sector size").map(String::as_str)) }),
375 );
376 }
377 result
380static EXT_STATE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^Filesystem state:\s*(.+)$").unwrap());
381static XFS_DATA: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^data\s*=.*?bsize=(\d+)\s+blocks=(\d+)").unwrap());
382static EXT_TYPE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^ext[234]$").unwrap());
384fn parse_fsinfo(section: &str) -> HashMap<String, Value> {
385 let mut result = HashMap::new();
386 for block in split_marker(section, "@@fs ") {
387 let (head, body) = head_body(block);
388 let mut words = head.split_whitespace();
389 let device = words.next().unwrap_or("undefined").to_string();
390 let kind = words.next().unwrap_or("undefined");
391 if EXT_TYPE.is_match(kind) {
392 let value = |key: &str| {
393 let re = Regex::new(&format!(r"(?m)^{}:\s*(\d+)", regex::escape(key))).unwrap();
394 int_str(re.captures(body).map(|c| c.get(1).unwrap().as_str()))
395 };
396 let (blocks, block_size, free) = (value("Block count"), value("Block size"), value("Free blocks"));
397 if truthy(&blocks) && truthy(&block_size) {
398 let (b, s) = (number(&blocks), number(&block_size));
399 let state = EXT_STATE.captures(body).map(|c| c[1].to_string()).unwrap_or_default();
400 result.insert(device, json!({ "fsSize": num(b * s), "fsUsed": if free.is_null() { Value::Null } else { num((b - number(&free)) * s) }, "blockSize": block_size, "state": text(&state, 40) }));
401 }
402 } else if kind == "xfs" {
403 if let Some(c) = XFS_DATA.captures(body) {
404 result.insert(device, json!({ "fsSize": num(number_str(&c[1]) * number_str(&c[2])), "blockSize": num(number_str(&c[1])) }));
405 }
406 }
407 }
408 result
411static BTRFS_LABEL: Lazy<Regex> = Lazy::new(|| Regex::new(r"^Label:\s*(?:'(.*)'|none)\s+uuid:\s*([0-9a-f-]+)").unwrap());
412static BTRFS_DEVICE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*devid\s+(\d+)\s+size\s+(\d+)\s+used\s+(\d+)\s+path\s+(\S+)").unwrap());
414fn parse_btrfs(section: &str) -> Vec<Value> {
415 let mut filesystems: Vec<Value> = Vec::new();
416 for line in lines(section) {
417 if let Some(c) = BTRFS_LABEL.captures(line) {
418 let label = c.get(1).map(|m| m.as_str()).filter(|s| !s.is_empty());
419 filesystems.push(json!({ "label": label, "uuid": &c[2], "devices": [] }));
420 continue;
421 }
422 if let (Some(c), Some(current)) = (BTRFS_DEVICE.captures(line), filesystems.last_mut()) {
423 current["devices"].as_array_mut().unwrap().push(json!({ "devid": num(number_str(&c[1])), "size": num(number_str(&c[2])), "used": num(number_str(&c[3])), "path": &c[4] }));
424 }
425 }
426 filesystems
429fn parse_zpool(section: &str) -> Vec<Value> {
430 let mut pools: Vec<Value> = Vec::new();
431 for line in lines(section) {
432 let f: Vec<&str> = line.split('\t').collect();
433 let get = |i: usize| f.get(i).copied();
434 if get(0).is_some_and(|s| !s.is_empty()) {
435 let health = get(9).filter(|s| !s.is_empty()).or_else(|| f.last().copied()).unwrap_or("");
436 pools.push(json!({ "name": text(get(0).unwrap(), 64), "size": int_str(get(1)), "allocated": int_str(get(2)), "free": int_str(get(3)), "health": text(health, 16), "devices": [] }));
437 } else if let (Some(current), Some(dev)) = (pools.last_mut(), get(1).filter(|d| d.starts_with("/dev/"))) {
438 current["devices"].as_array_mut().unwrap().push(json!(text(dev, 200)));
439 }
440 }
441 pools
444fn parse_lsblk(section: Option<&str>) -> HashMap<String, Value> {
445 let json = parse_json(section.unwrap_or(""));
446 let mut map = HashMap::new();
447 for entry in json.get("blockdevices").and_then(Value::as_array).cloned().unwrap_or_default() {
448 let key = [&entry["kname"], &entry["name"]].into_iter().find(|v| truthy(v)).map(|v| js::string(Some(v)));
449 if let Some(key) = key {
450 map.insert(key, entry);
451 }
452 }
453 map
456fn media_type(entry: &SysEntry) -> &'static str {
457 if entry.kname.starts_with("loop") {
458 return "loop";
459 }
460 if entry.kname.starts_with("nvme") {
461 return if VIRTUAL_MODEL.is_match(&entry.model) { "virtual" } else { "nvme" };
462 }
463 if entry.kname.starts_with("vd") || entry.kname.starts_with("xvd") || VIRTUAL_MODEL.is_match(&entry.model) || VIRTUAL_MODEL.is_match(&entry.vendor) {
464 return "virtual";
465 }
466 match entry.rotational {
467 Value::Bool(true) => "hdd",
468 Value::Bool(false) => "ssd",
469 _ => "unknown",
470 }
473static T_VIRTIO_BLOCK: Lazy<Regex> = Lazy::new(|| Regex::new(r"/virtio\d+/block/").unwrap());
474static T_VIRTIO_SCSI: Lazy<Regex> = Lazy::new(|| Regex::new(r"/virtio\d+/host\d+/").unwrap());
475static T_USB: Lazy<Regex> = Lazy::new(|| Regex::new(r"/usb\d+/").unwrap());
476static T_ATA: Lazy<Regex> = Lazy::new(|| Regex::new(r"/ata\d+/").unwrap());
477static T_SCSI: Lazy<Regex> = Lazy::new(|| Regex::new(r"/host\d+/target").unwrap());
479fn transport(entry: &SysEntry, lsblk: &Value) -> Value {
480 if truthy(&lsblk["tran"]) {
481 return json!(text(&js::string(Some(&lsblk["tran"])), 16));
482 }
483 let path = &entry.sys_path;
484 let t = if entry.kname.starts_with("nvme") {
485 "nvme"
486 } else if T_VIRTIO_BLOCK.is_match(path) {
487 "virtio"
488 } else if T_VIRTIO_SCSI.is_match(path) {
489 "virtio-scsi"
490 } else if T_USB.is_match(path) {
491 "usb"
492 } else if T_ATA.is_match(path) {
493 "sata"
494 } else if T_SCSI.is_match(path) {
495 "scsi"
496 } else if entry.kname.starts_with("mmcblk") {
497 "mmc"
498 } else {
499 return Value::Null;
500 };
501 json!(t)
504static MD: Lazy<Regex> = Lazy::new(|| Regex::new(r"^md\d").unwrap());
506fn node_kind(entry: &SysEntry) -> &'static str {
507 if !entry.partition.is_null() {
508 return "part";
509 }
510 if entry.kname.starts_with("dm-") {
511 if entry.dm_uuid.starts_with("CRYPT-") {
512 return "crypt";
513 }
514 if entry.dm_uuid.starts_with("LVM-") {
515 return "lvm";
516 }
517 return "dm";
518 }
519 if MD.is_match(&entry.kname) {
520 return "md";
521 }
522 if entry.kname.starts_with("loop") {
523 return "loop";
524 }
525 "disk"
528static DM_LV: Lazy<Regex> = Lazy::new(|| Regex::new(r"^((?:[^-]|--)+)-((?:[^-]|--)+)$").unwrap());
530/// Unescape a device-mapper LVM name ("vg--data-lv" → vg "vg-data", lv "lv").
531pub fn split_dm_lv_name(name: &str) -> Option<(String, String)> {
532 // The JavaScript regex backtracks to the last single dash between two valid halves.
533 let chars: Vec<char> = name.chars().collect();
534 for i in (0..chars.len()).rev() {
535 if chars[i] != '-' {
536 continue;
537 }
538 let (left, right): (String, String) = (chars[..i].iter().collect(), chars[i + 1..].iter().collect());
539 let valid = |s: &str| !s.is_empty() && DM_LV.is_match(&format!("{s}-x")) && !s.starts_with('-') && dashes_paired(s);
540 if valid(&left) && valid(&right) {
541 return Some((left.replace("--", "-"), right.replace("--", "-")));
542 }
543 }
544 None
547/// `(?:[^-]|--)+` matches the whole string.
548fn dashes_paired(s: &str) -> bool {
549 let mut run = 0;
550 for c in s.chars() {
551 if c == '-' {
552 run += 1;
553 } else {
554 if run % 2 == 1 {
555 return false;
556 }
557 run = 0;
558 }
559 }
560 run % 2 == 0
563static TPOOL: Lazy<Regex> = Lazy::new(|| Regex::new(r"-tpool$|_tdata$|_tmeta$").unwrap());
564static CRYPT_TYPE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^CRYPT-([A-Z0-9]+)-").unwrap());
565static MD_STAT: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b(md\d+)\b").unwrap());
566static SKIP_KNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(ram|zram|fd)\d*$").unwrap());
567static LOOP_N: Lazy<Regex> = Lazy::new(|| Regex::new(r"^loop\d+$").unwrap());
568static SR_N: Lazy<Regex> = Lazy::new(|| Regex::new(r"^sr\d+$").unwrap());
569static PART_DIGITS: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)$").unwrap());
570static RO: Lazy<Regex> = Lazy::new(|| Regex::new(r"(^|,)ro(,|$)").unwrap());
571static LEADING_ZEROS: Lazy<Regex> = Lazy::new(|| Regex::new(r"^0+(.)").unwrap());
573fn get_str<'a>(map: &'a BTreeMap<String, String>, key: &str) -> Option<&'a str> {
574 map.get(key).map(String::as_str)
577pub fn build_inventory(stdout: &str) -> Value {
578 let sections = split_sections(stdout);
579 let section = |name: &str| sections.get(name).map(String::as_str);
580 let s = |name: &str| section(name).unwrap_or("");
581 let meta = key_values(s("meta"));
582 let tools = key_values(s("tools"));
583 let versions = key_values(s("versions"));
584 let privileged = get_str(&meta, "uid") == Some("0");
585 let sys = parse_sysblock(s("sysblock"));
586 let lsblk = parse_lsblk(section("lsblk"));
587 let blkid = parse_blkid(s("blkid"));
588 let mounts = parse_mountinfo(s("mountinfo"));
589 let df = parse_df(s("df"));
590 let swaps = parse_swaps(s("swaps"));
591 let tables = parse_partition_tables(s("pt"));
592 let lvm = parse_lvm(section("lvm"));
593 let crypt = parse_crypt(s("crypt"));
594 let fsinfo = parse_fsinfo(s("fsinfo"));
595 let btrfs = parse_btrfs(s("btrfs"));
596 let zpools = parse_zpool(s("zpool"));
597 let mdstat = s("mdstat").to_string();
599 // Nodes in insertion order; `_sys`/`_lsblk` kept beside them.
600 let mut order: Vec<String> = Vec::new();
601 let mut nodes: HashMap<String, Map<String, Value>> = HashMap::new();
602 let mut internal: HashMap<String, (SysEntry, Value)> = HashMap::new();
603 for entry in &sys {
604 if SKIP_KNAME.is_match(&entry.kname) {
605 continue;
606 }
607 // Unused loop devices (size 0) and read-only snap images are not disks anyone manages here.
608 if LOOP_N.is_match(&entry.kname) && (!truthy(&entry.sectors512) || entry.read_only || entry.loop_backing.is_empty()) {
609 continue;
610 }
611 if SR_N.is_match(&entry.kname) {
612 continue;
613 }
614 let ls = lsblk.get(&entry.kname).cloned().unwrap_or_else(|| json!({}));
615 let path = if !entry.dm_name.is_empty() && entry.kname.starts_with("dm-") { format!("/dev/mapper/{}", entry.dm_name) } else { format!("/dev/{}", entry.kname) };
616 let empty = BTreeMap::new();
617 let probe = blkid.get(&format!("/dev/{}", entry.kname)).or_else(|| blkid.get(&path)).unwrap_or(&empty);
618 let kind = node_kind(entry);
619 let probe_or = |key: &str, ls_key: &str| -> String {
620 // `probe.X ?? ls.x ?? ""`
621 match probe.get(key) {
622 Some(v) => v.clone(),
623 None => match &ls[ls_key] {
624 Value::Null => String::new(),
625 v => js::string(Some(v)),
626 },
627 }
628 };
629 let probe_truthy = |key: &str, ls_key: &str| -> String {
630 // `probe.X || ls.x || ""`
631 match probe.get(key).filter(|v| !v.is_empty()) {
632 Some(v) => v.clone(),
633 None if truthy(&ls[ls_key]) => js::string(Some(&ls[ls_key])),
634 None => String::new(),
635 }
636 };
637 let fstype = text_or_null(&probe_truthy("TYPE", "fstype"), 32);
638 let mut node = Map::new();
639 node.insert("kname".into(), json!(entry.kname));
640 node.insert("path".into(), json!(path));
641 node.insert("kind".into(), json!(kind));
642 node.insert("devno".into(), json!(entry.devno));
643 node.insert("size".into(), if entry.sectors512.is_null() { Value::Null } else { num(number(&entry.sectors512) * 512.0) });
644 node.insert("readOnly".into(), json!(entry.read_only));
645 node.insert("parent".into(), entry.parent.clone());
646 node.insert("partition".into(), entry.partition.clone());
647 node.insert("start512".into(), entry.start512.clone());
648 node.insert("fstype".into(), fstype);
649 node.insert("fsVersion".into(), text_or_null(&probe_truthy("VERSION", "fsver"), 16));
650 node.insert("label".into(), text_or_null(&probe_or("LABEL", "label"), 64));
651 node.insert("uuid".into(), text_or_null(&probe_or("UUID", "uuid"), 64));
652 node.insert("partuuid".into(), text_or_null(&probe_or("PARTUUID", "partuuid"), 64));
653 node.insert("partlabel".into(), text_or_null(&probe_or("PARTLABEL", "partlabel"), 72));
654 node.insert("parttype".into(), text_or_null(&if ls["parttype"].is_null() { String::new() } else { js::string(Some(&ls["parttype"])) }, 40));
655 node.insert("dmName".into(), if entry.dm_name.is_empty() { Value::Null } else { json!(entry.dm_name) });
656 node.insert("dmUuid".into(), if entry.dm_uuid.is_empty() { Value::Null } else { json!(entry.dm_uuid) });
657 node.insert("loopBacking".into(), if entry.loop_backing.is_empty() { Value::Null } else { json!(entry.loop_backing) });
658 node.insert("mdLevel".into(), if entry.md_level.is_empty() { Value::Null } else { json!(entry.md_level) });
659 node.insert("mountpoints".into(), json!([]));
660 node.insert("usage".into(), Value::Null);
661 node.insert("fsSize".into(), Value::Null);
662 node.insert("fsUsed".into(), Value::Null);
663 node.insert("swapActive".into(), json!(false));
664 node.insert("lvm".into(), Value::Null);
665 node.insert("crypt".into(), Value::Null);
666 node.insert("zfsPool".into(), Value::Null);
667 node.insert("btrfs".into(), Value::Null);
668 if !nodes.contains_key(&entry.kname) {
669 order.push(entry.kname.clone());
670 }
671 nodes.insert(entry.kname.clone(), node);
672 internal.insert(entry.kname.clone(), (entry.clone(), ls));
673 }
674 let lookup_path = |nodes: &HashMap<String, Map<String, Value>>, order: &[String]| {
675 let mut by_path: HashMap<String, String> = HashMap::new();
676 for kname in order {
677 let node = &nodes[kname];
678 by_path.insert(js::string(node.get("path")), kname.clone());
679 by_path.insert(format!("/dev/{kname}"), kname.clone());
680 if truthy(&node["dmName"]) {
681 by_path.insert(format!("/dev/mapper/{}", js::string(node.get("dmName"))), kname.clone());
682 }
683 }
684 by_path
685 };
686 let mut by_devno: HashMap<String, String> = HashMap::new();
687 for kname in &order {
688 by_devno.insert(js::string(nodes[kname].get("devno")), kname.clone());
689 }
690 let by_path = lookup_path(&nodes, &order);
692 // Mounts: by device number first (exact), by source path otherwise (Btrfs uses anonymous device numbers).
693 for mount in &mounts {
694 let Some(kname) = by_devno.get(&mount.devno).or_else(|| by_path.get(&mount.source)).cloned() else { continue };
695 let node = nodes.get_mut(&kname).unwrap();
696 let points = node["mountpoints"].as_array_mut().unwrap();
697 if !points.iter().any(|p| p == mount.mountpoint.as_str()) {
698 points.push(json!(mount.mountpoint));
699 }
700 if !node.get("mountedFstype").is_some_and(truthy) {
701 node.insert("mountedFstype".into(), json!(mount.fstype));
702 }
703 node.insert("mountedReadOnly".into(), json!(RO.is_match(&mount.options)));
704 }
705 for swap in &swaps {
706 if let Some(kname) = by_path.get(&swap.path) {
707 let node = nodes.get_mut(kname).unwrap();
708 node.insert("swapActive".into(), json!(true));
709 node.insert("swapUsed".into(), swap.used.clone());
710 }
711 }
712 for kname in &order {
713 let node = nodes.get_mut(kname).unwrap();
714 let mountpoint = node["mountpoints"].as_array().unwrap().iter().filter_map(Value::as_str).find(|p| df.contains_key(*p)).map(str::to_string);
715 if let Some(mountpoint) = mountpoint {
716 let mut usage = df[&mountpoint].as_object().cloned().unwrap();
717 usage.insert("source".into(), json!("df"));
718 usage.insert("mountpoint".into(), json!(mountpoint));
719 node.insert("usage".into(), Value::Object(usage));
720 }
721 let info = fsinfo.get(&js::string(node.get("path"))).or_else(|| fsinfo.get(&format!("/dev/{kname}")));
722 if let Some(info) = info {
723 node.insert("fsSize".into(), info["fsSize"].clone());
724 node.insert("fsState".into(), if truthy(&info["state"]) { info["state"].clone() } else { Value::Null });
725 let fs_used = info.get("fsUsed");
726 if !truthy(&node["usage"]) && fs_used.is_some_and(|u| !u.is_null()) {
727 let (size, used) = (number(&info["fsSize"]), number(fs_used.unwrap()));
728 node.insert("usage".into(), json!({ "size": info["fsSize"], "used": fs_used.unwrap(), "available": num(size - used), "source": "superblock" }));
729 }
730 }
731 if !truthy(&node["fsSize"]) && truthy(&node["usage"]) && node["fstype"] == "xfs" {
732 node.insert("fsSize".into(), Value::Null);
733 }
734 }
735 for fs in &btrfs {
736 let devices = fs["devices"].as_array().cloned().unwrap_or_default();
737 for device in &devices {
738 if let Some(kname) = by_path.get(&js::string(device.get("path"))) {
739 let node = nodes.get_mut(kname).unwrap();
740 node.insert("btrfs".into(), json!({ "uuid": fs["uuid"], "devid": device["devid"], "devices": devices.len(), "allocated": device["used"] }));
741 node.insert("fsSize".into(), device["size"].clone());
742 }
743 }
744 }
745 for pool in &zpools {
746 for device in pool["devices"].as_array().cloned().unwrap_or_default() {
747 if let Some(kname) = by_path.get(&js::string(Some(&device))) {
748 nodes.get_mut(kname).unwrap().insert("zfsPool".into(), pool["name"].clone());
749 }
750 }
751 }
753 // LVM: privileged JSON when available, otherwise the device-mapper name.
754 let mut pv_by_path: HashMap<String, Value> = HashMap::new();
755 for pv in lvm["pvs"].as_array().cloned().unwrap_or_default() {
756 pv_by_path.insert(js::string(pv.get("path")), pv);
757 }
758 let lvs = lvm["lvs"].as_array().cloned().unwrap_or_default();
759 for kname in &order {
760 let node = nodes.get_mut(kname).unwrap();
761 let path = js::string(node.get("path"));
762 if let Some(pv) = pv_by_path.get(&path).or_else(|| pv_by_path.get(&format!("/dev/{kname}"))) {
763 node.insert("pv".into(), pv.clone());
764 }
765 let dm_name = js::string(Some(&node["dmName"]));
766 let dm_name_js = if node["dmName"].is_null() { "null".to_string() } else { dm_name.clone() };
767 if node["kind"] == "lvm" {
768 let lv = lvs.iter().find(|e| e["dmPath"] == path.as_str() || e["dmPath"] == format!("/dev/mapper/{dm_name_js}").as_str());
769 let names = split_dm_lv_name(&dm_name_js);
770 let value = match (lv, names) {
771 (Some(lv), _) => lv.clone(),
772 (None, Some((vg, lv_name))) => json!({ "name": lv_name, "vg": vg, "size": node["size"], "attr": null, "thinPool": TPOOL.is_match(&lv_name), "thinVolume": false, "partial": true }),
773 _ => Value::Null,
774 };
775 if !value.is_null() {
776 node.insert("displayPath".into(), json!(format!("/dev/{}/{}", js::string(value.get("vg")), js::string(value.get("name")))));
777 }
778 node.insert("lvm".into(), value);
779 }
780 if node["kind"] == "crypt" {
781 let status = crypt.get(&dm_name_js);
782 let dm_uuid = if node["dmUuid"].is_null() { "null".to_string() } else { js::string(Some(&node["dmUuid"])) };
783 let kind = CRYPT_TYPE.captures(&dm_uuid).map(|c| json!(&c[1])).unwrap_or(Value::Null);
784 let pick = |k: &str| status.map(|s| s[k].clone()).filter(truthy).unwrap_or(Value::Null);
785 let crypt_type = status.map(|s| s["type"].clone()).filter(truthy).unwrap_or(kind);
786 node.insert("crypt".into(), json!({ "type": crypt_type, "cipher": pick("cipher"), "keyLocation": pick("keyLocation"), "mode": pick("mode"), "open": true }));
787 }
788 }
790 // Graph edges. Partitions hang below their disk; dm/md below their slaves.
791 for kname in &order {
792 let (entry, _) = &internal[kname];
793 let parents: Vec<String> = if nodes[kname]["kind"] == "part" {
794 entry.parent.as_str().filter(|p| nodes.contains_key(*p)).map(|p| vec![p.to_string()]).unwrap_or_default()
795 } else {
796 entry.slaves.iter().filter(|n| nodes.contains_key(*n)).cloned().collect()
797 };
798 let children: Vec<String> = entry.holders.iter().filter(|n| nodes.contains_key(*n)).cloned().collect();
799 let node = nodes.get_mut(kname).unwrap();
800 node.insert("parents".into(), json!(parents));
801 node.insert("children".into(), json!(children));
802 }
803 fn descendants(nodes: &HashMap<String, Map<String, Value>>, kname: &str, seen: &mut Vec<String>) {
804 for child in nodes[kname]["children"].as_array().unwrap().iter().filter_map(Value::as_str) {
805 if !seen.iter().any(|s| s == child) {
806 seen.push(child.to_string());
807 descendants(nodes, child, seen);
808 }
809 }
810 }
811 let raid_running = MD_STAT.is_match(&mdstat);
812 for kname in order.clone() {
813 let mut reasons: Vec<Value> = Vec::new();
814 let node = &nodes[&kname];
815 let points: Vec<String> = node["mountpoints"].as_array().unwrap().iter().filter_map(|v| v.as_str().map(str::to_string)).collect();
816 if !points.is_empty() {
817 reasons.push(json!({ "reason": "mounted", "detail": points.join(", ") }));
818 }
819 if node["swapActive"] == true {
820 reasons.push(json!({ "reason": "swap" }));
821 }
822 let children: Vec<String> = node["children"].as_array().unwrap().iter().filter_map(|v| v.as_str().map(str::to_string)).collect();
823 if !children.is_empty() {
824 let detail = children.iter().map(|c| { let n = &nodes[c]; js::string(n.get("displayPath").filter(|v| truthy(v)).or(n.get("path"))) }).collect::<Vec<_>>().join(", ");
825 reasons.push(json!({ "reason": "holders", "detail": detail }));
826 }
827 if node["fstype"] == "zfs_member" {
828 reasons.push(json!({ "reason": "zfs", "detail": if truthy(&node["zfsPool"]) { js::string(Some(&node["zfsPool"])) } else { String::new() } }));
829 }
830 if node["fstype"] == "linux_raid_member" && children.is_empty() && raid_running {
831 reasons.push(json!({ "reason": "raid-member" }));
832 }
833 let mut stack = vec![kname.clone()];
834 let mut seen = Vec::new();
835 descendants(&nodes, &kname, &mut seen);
836 stack.extend(seen);
837 let mut roles: Vec<String> = Vec::new();
838 for item in &stack {
839 let n = &nodes[item];
840 for point in n["mountpoints"].as_array().unwrap().iter().filter_map(Value::as_str) {
841 if SYSTEM_MOUNTS.is_match(point) {
842 let role = format!("mount:{point}");
843 if !roles.contains(&role) {
844 roles.push(role);
845 }
846 }
847 }
848 if n["swapActive"] == true && !roles.iter().any(|r| r == "swap") {
849 roles.push("swap".into());
850 }
851 }
852 let node = nodes.get_mut(&kname).unwrap();
853 node.insert("inUse".into(), json!(!reasons.is_empty()));
854 node.insert("inUseReasons".into(), json!(reasons));
855 node.insert("systemRoles".into(), json!(roles));
856 }
858 let mut disks: Vec<Value> = Vec::new();
859 for kname in &order {
860 let node = &nodes[kname];
861 if !(node["kind"] == "disk" || node["kind"] == "loop") || !truthy(&node["size"]) {
862 continue;
863 }
864 let (entry, ls) = &internal[kname];
865 let table = tables.get(kname);
866 let lbs = table.map(|t| number(&t["sectorsize"])).filter(|n| *n != 0.0 && !n.is_nan()).unwrap_or(if entry.logical_sector_size != 0.0 { entry.logical_sector_size } else { 512.0 });
867 let size = number(&node["size"]);
868 let sectors = (size / lbs).floor();
869 let align = (MIB / lbs).floor().max(1.0);
870 let mut part_nodes: Vec<&Map<String, Value>> = order.iter().map(|k| &nodes[k]).filter(|n| n["kind"] == "part" && n["parent"] == kname.as_str()).collect();
871 let mut label: Option<String> = table.map(|t| js::string(t.get("label"))).filter(|l| !l.is_empty()).or_else(|| Some(text(&if truthy(&ls["pttype"]) { js::string(Some(&ls["pttype"])) } else { String::new() }, 8)).filter(|l| !l.is_empty()));
872 if label.is_none() && !part_nodes.is_empty() {
873 label = Some("unknown".into());
874 }
875 let label_str = label.clone().unwrap_or_default();
876 let mut partitions: Vec<Value> = Vec::new();
877 if let Some(table) = table {
878 let mut parts = table["partitions"].as_array().cloned().unwrap_or_default();
879 parts.sort_by(|a, b| number(&a["start"]).partial_cmp(&number(&b["start"])).unwrap_or(std::cmp::Ordering::Equal));
880 for part in &parts {
881 let number_value = PART_DIGITS.captures(&js::string(part.get("node"))).map(|c| number_str(&c[1])).unwrap_or(f64::NAN);
882 let kernel = part_nodes.iter().find(|n| number(&n["partition"]) == number_value && !n["partition"].is_null()).copied();
883 partitions.push(describe_partition(&label_str, number_value, number(&part["start"]), number(&part["size"]), lbs, part, kernel));
884 }
885 } else {
886 part_nodes.sort_by(|a, b| number(&a["start512"]).partial_cmp(&number(&b["start512"])).unwrap_or(std::cmp::Ordering::Equal));
887 let factor = lbs / 512.0;
888 for part in &part_nodes {
889 let synthetic = json!({ "type": part["parttype"], "uuid": part["partuuid"], "name": part["partlabel"] });
890 partitions.push(describe_partition(&label_str, number(&part["partition"]), js_round(number(&part["start512"]) / factor), js_round(number(&part["size"]) / lbs), lbs, &synthetic, Some(part)));
891 }
892 }
893 // Usable area of the table; GPT keeps 33 sectors (header + 128 entries) at both ends.
894 let gpt = label_str == "gpt";
895 let gpt_tail = (16384.0 / lbs).ceil() + 1.0;
896 let first_lba = table.map(|t| js::number_opt(t.get("firstlba"))).filter(|n| *n != 0.0 && !n.is_nan());
897 let last_lba = table.map(|t| js::number_opt(t.get("lastlba"))).filter(|n| *n != 0.0 && !n.is_nan());
898 let first_usable = if gpt { first_lba.unwrap_or(gpt_tail + 1.0) } else { align };
899 let last_usable_now = if gpt { last_lba.unwrap_or(sectors - gpt_tail - 1.0) } else { (sectors - 1.0).min(4_294_967_295.0) };
900 let last_usable = if gpt { sectors - gpt_tail - 1.0 } else { last_usable_now };
901 let gpt_backup_misplaced = gpt && table.is_some_and(|t| truthy_opt_value(t.get("lastlba"))) && last_usable_now < last_usable;
902 let mbr_limited = label_str == "dos" && sectors - 1.0 > 4_294_967_295.0;
903 let segments = if label.is_some() { build_segments(&partitions, first_usable, last_usable, align, lbs, last_usable_now) } else { Vec::new() };
904 let mut all_roles: Vec<Value> = Vec::new();
905 let mut sources: Vec<&Map<String, Value>> = vec![node];
906 for part in &partitions {
907 if let Some(kernel) = part["kernel"].as_str() {
908 if let Some(n) = nodes.get(kernel) {
909 sources.push(n);
910 }
911 }
912 }
913 for source in sources {
914 for role in source["systemRoles"].as_array().unwrap() {
915 if !all_roles.contains(role) {
916 all_roles.push(role.clone());
917 }
918 }
919 }
920 let table_value = label.as_ref().map(|l| {
921 let id_source = table.map(|t| t["id"].clone()).filter(truthy).or_else(|| Some(ls["ptuuid"].clone()).filter(truthy)).map(|v| js::string(Some(&v))).unwrap_or_default();
922 json!({
923 "type": l, "id": text_or_null(&id_source, 64), "firstUsable": num(first_usable), "lastUsable": num(last_usable), "lastUsableNow": num(last_usable_now),
924 "hash": table.map(|t| t["hash"].clone()).filter(truthy).unwrap_or(Value::Null), "source": if table.is_some() { "sfdisk" } else { "sysfs" },
925 "gptBackupMisplaced": gpt_backup_misplaced, "mbrLimited": mbr_limited, "maxPrimary": if l == "dos" { 4 } else { 128 }
926 })
927 });
928 let serial = if !entry.serial.is_empty() { json!(entry.serial) } else { text_or_null(&if truthy(&ls["serial"]) { js::string(Some(&ls["serial"])) } else { String::new() }, 80) };
929 let wwid = if !entry.wwid.is_empty() { json!(entry.wwid) } else { text_or_null(&if truthy(&ls["wwn"]) { js::string(Some(&ls["wwn"])) } else { String::new() }, 120) };
930 let in_use = node["inUse"] == true || partitions.iter().any(|p| p["inUse"] == true);
931 disks.push(json!({
932 "name": kname, "path": node["path"], "kind": node["kind"],
933 "model": if entry.model.is_empty() { Value::Null } else { json!(entry.model) },
934 "vendor": if entry.vendor.is_empty() { Value::Null } else { json!(entry.vendor) },
935 "serial": serial, "wwid": wwid,
936 "transport": transport(entry, ls), "mediaType": media_type(entry), "rotational": entry.rotational,
937 "discard": number(&entry.discard_max) > 0.0, "readOnly": node["readOnly"], "removable": entry.removable, "loopBacking": node["loopBacking"],
938 "size": node["size"], "sectors": num(sectors), "logicalSectorSize": num(lbs), "physicalSectorSize": num(entry.physical_sector_size), "alignSectors": num(align),
939 "table": table_value.unwrap_or(Value::Null),
940 "partitions": partitions, "segments": segments,
941 "wholeDevice": Value::Null,
942 "systemRoles": all_roles.clone(), "system": !all_roles.is_empty(),
943 "inUse": in_use
944 }));
945 let last = disks.last_mut().unwrap();
946 if last["partitions"].as_array().unwrap().is_empty() {
947 last["wholeDevice"] = json!({ "kname": kname, "fstype": node["fstype"] });
948 }
949 }
951 let pve = get_str(&meta, "pve") == Some("1");
952 for disk in disks.iter_mut() {
953 let mut stack: Vec<String> = vec![js::string(disk.get("name"))];
954 for part in disk["partitions"].as_array().unwrap() {
955 if let Some(k) = part["kernel"].as_str() {
956 if !stack.iter().any(|s| s == k) {
957 stack.push(k.to_string());
958 }
959 }
960 }
961 for kname in stack.clone() {
962 let mut seen = Vec::new();
963 if nodes.contains_key(&kname) {
964 descendants(&nodes, &kname, &mut seen);
965 }
966 for child in seen {
967 if !stack.contains(&child) {
968 stack.push(child);
969 }
970 }
971 }
972 let stack_nodes: Vec<&Map<String, Value>> = stack.iter().filter_map(|k| nodes.get(k)).collect();
973 let any = |f: &dyn Fn(&Map<String, Value>) -> bool| stack_nodes.iter().any(|n| f(n));
974 let layers = json!({
975 "luks": any(&|n| n["fstype"] == "crypto_LUKS"),
976 "lvm": any(&|n| n["fstype"] == "LVM2_member" || n["kind"] == "lvm"),
977 "raid": any(&|n| n["fstype"] == "linux_raid_member" || n["kind"] == "md"),
978 "zfs": any(&|n| n["fstype"] == "zfs_member")
979 });
980 // Proxmox host storage (LVM/ZFS behind guests or the host) is never a target of destructive changes.
981 let protected = pve && (layers["lvm"] == true || layers["zfs"] == true || disk["system"] == true);
982 disk["layers"] = layers;
983 disk["protected"] = if protected { json!("pve-host-storage") } else { Value::Null };
984 }
986 let public_nodes: Vec<Value> = order.iter().map(|k| Value::Object(nodes[k].clone())).collect();
987 disks.sort_by(|a, b| js::natural_cmp(a["name"].as_str().unwrap_or(""), b["name"].as_str().unwrap_or("")));
988 let clean_map = |m: &BTreeMap<String, String>| Value::Object(m.iter().map(|(k, v)| (text(k, 32), json!(text(v, 120)))).collect());
989 let lsblk_present = !lsblk.is_empty();
990 json!({
991 "platform": "linux", "supported": true, "privileged": privileged,
992 "os": text(get_str(&meta, "os").unwrap_or(""), 120), "osId": text(get_str(&meta, "osId").unwrap_or(""), 40), "osLike": text(get_str(&meta, "osLike").unwrap_or(""), 80), "kernel": text(get_str(&meta, "kernel").unwrap_or(""), 80),
993 "systemd": get_str(&meta, "systemd") == Some("1"), "pve": pve,
994 "selinux": get_str(&meta, "selinux").filter(|s| !s.is_empty()).map(|s| json!(text(s, 20))).unwrap_or(Value::Null),
995 "tools": clean_map(&tools),
996 "versions": clean_map(&versions),
997 "sources": { "lsblk": lsblk_present, "blkid": !blkid.is_empty(), "sfdisk": !tables.is_empty(), "lvm": lvm["available"], "crypt": !crypt.is_empty() },
998 "partial": !privileged,
999 "disks": disks,
1000 "nodes": public_nodes,
1001 "lvm": lvm,
1002 "btrfs": btrfs, "zpools": zpools,
1003 "remoteTime": int_str(get_str(&meta, "time"))
1004 })
1007fn truthy_opt_value(value: Option<&Value>) -> bool {
1008 value.is_some_and(truthy)
1011/// JavaScript `Math.round` (halves round up).
1012pub fn js_round(value: f64) -> f64 {
1013 (value + 0.5).floor()
1016fn describe_partition(table_type: &str, number_value: f64, start: f64, size: f64, lbs: f64, part: &Value, kernel: Option<&Map<String, Value>>) -> Value {
1017 let raw_type = if truthy(&part["type"]) { js::string(Some(&part["type"])) } else { String::new() };
1018 let type_code = text(&raw_type, 40).to_uppercase();
1019 let type_name = if table_type == "gpt" {
1020 gpt_type(&type_code)
1021 } else {
1022 let lower = type_code.to_lowercase();
1023 dos_type(&LEADING_ZEROS.replace(&lower, "$1"))
1024 };
1025 let extended = type_name == Some("extended");
1026 let k = |key: &str| kernel.map(|n| n[key].clone()).unwrap_or(Value::Null);
1027 let pick = |own: &str, kernel_key: &str, limit: usize| {
1028 let source = if truthy(&part[own]) { js::string(Some(&part[own])) } else if truthy(&k(kernel_key)) { js::string(Some(&k(kernel_key))) } else { String::new() };
1029 text_or_null(&source, limit)
1030 };
1031 let attrs = if truthy(&part["attrs"]) { js::string(Some(&part["attrs"])) } else { String::new() };
1032 json!({
1033 "number": num(number_value), "start": num(start), "size": num(size), "end": num(start + size - 1.0), "bytes": num(size * lbs),
1034 "type": if type_code.is_empty() { Value::Null } else { json!(type_code) }, "typeName": type_name,
1035 "uuid": pick("uuid", "partuuid", 64), "name": pick("name", "partlabel", 72),
1036 "attrs": text_or_null(&attrs, 160), "bootable": part["bootable"] == true,
1037 "extended": extended, "logical": table_type == "dos" && number_value > 4.0,
1038 "kernel": k("kname"), "path": k("path"),
1039 "kernelMatches": kernel.is_some_and(|n| js_round(number(&n["size"]) / lbs) == size || extended),
1040 "fstype": k("fstype"), "label": k("label"), "fsUuid": k("uuid"),
1041 "mountpoints": kernel.map(|n| n["mountpoints"].clone()).filter(truthy).unwrap_or_else(|| json!([])),
1042 "usage": k("usage"), "fsSize": if truthy(&k("fsSize")) { k("fsSize") } else { Value::Null },
1043 "inUse": kernel.is_some_and(|n| n["inUse"] == true),
1044 "inUseReasons": kernel.map(|n| n["inUseReasons"].clone()).filter(truthy).unwrap_or_else(|| json!([])),
1045 "systemRoles": kernel.map(|n| n["systemRoles"].clone()).filter(truthy).unwrap_or_else(|| json!([])),
1046 "children": kernel.map(|n| n["children"].clone()).filter(truthy).unwrap_or_else(|| json!([]))
1047 })
1050/// Ordered partitions and unallocated gaps (≥ one alignment unit) between firstUsable and lastUsable.
1051pub fn build_segments(partitions: &[Value], first_usable: f64, last_usable: f64, align: f64, lbs: f64, last_usable_now: f64) -> Vec<Value> {
1052 let mut top: Vec<&Value> = partitions.iter().filter(|p| p["logical"] != true).collect();
1053 top.sort_by(|a, b| number(&a["start"]).partial_cmp(&number(&b["start"])).unwrap_or(std::cmp::Ordering::Equal));
1054 let mut segments = Vec::new();
1055 let mut cursor = first_usable;
1056 let push_free = |from: f64, to: f64, segments: &mut Vec<Value>| {
1057 if to - from + 1.0 < align {
1058 return;
1060 segments.push(json!({ "kind": "free", "start": num(from), "size": num(to - from + 1.0), "end": num(to), "bytes": num((to - from + 1.0) * lbs), "needsGptRelocation": to > last_usable_now }));
1061 };
1062 for part in top {
1063 let (start, end) = (number(&part["start"]), number(&part["end"]));
1064 if start > cursor {
1065 push_free(cursor, start - 1.0, &mut segments);
1067 segments.push(json!({ "kind": "partition", "number": part["number"], "start": part["start"], "size": part["size"], "end": part["end"], "bytes": part["bytes"] }));
1068 cursor = cursor.max(end + 1.0);
1070 if cursor <= last_usable {
1071 push_free(cursor, last_usable, &mut segments);
1073 segments
SHA-256fbf9e93eda5576e95cfae3d3fd9de3e399185b1133a6b7d3a63fdebe36de51ec

CH-J Proprietary Software License 1.14 · সোর্স CH-J Proprietary Software License 1.14-এর অধীনে দেওয়া হয়। এর প্রাপ্যতা লাইসেন্সের শর্ত বদলায় না এবং অতিরিক্ত কোনো অধিকারও দেয় না।

আর্কাইভ SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
ফাইল