diff --git a/Cargo.lock b/Cargo.lock index 60425cf..c8418bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "cc" -version = "1.2.17" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -270,9 +270,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -381,9 +381,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -391,7 +391,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.0", ] [[package]] @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", @@ -457,7 +457,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "libmount-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e0e42bbd34fa3f8fe6c9ffeb27906263d59d680825e4e71a5dacd7613e90a" +dependencies = [ + "bindgen", + "cc", + "walkdir", ] [[package]] @@ -572,9 +583,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "objc2-core-foundation" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags", ] @@ -679,9 +690,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", "syn", @@ -689,9 +700,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -848,9 +859,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags", "errno", @@ -945,9 +956,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -998,7 +1009,7 @@ dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.0.3", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -1008,7 +1019,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.0.3", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -1240,8 +1251,9 @@ name = "uu_lslocks" version = "0.0.1" dependencies = [ "clap", - "serde", - "serde_json", + "libc", + "libmount-sys", + "smartcols-sys", "uucore", ] @@ -1460,7 +1472,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1481,23 +1493,27 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.57.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-targets 0.52.6", + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings", ] [[package]] @@ -1511,6 +1527,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -1522,6 +1549,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.1" @@ -1537,6 +1575,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1701,7 +1757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix 1.0.3", + "rustix 1.0.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3798c36..7935ebb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ clap_mangen = "0.2" dns-lookup = "2.0.4" errno = "0.3" libc = "0.2.171" +libmount-sys = "0.1.1" linux-raw-sys = { version = "0.9.0", features = ["ioctl"] } md-5 = "0.10.6" nix = { version = "0.29", default-features = false } diff --git a/src/uu/lslocks/Cargo.toml b/src/uu/lslocks/Cargo.toml index 4d2a756..4761ac7 100644 --- a/src/uu/lslocks/Cargo.toml +++ b/src/uu/lslocks/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "uu_lslocks" +name = "uu_lslocks" version = "0.0.1" -edition = "2021" +edition = "2024" [lib] path = "src/lslocks.rs" @@ -11,7 +11,8 @@ name = "lslocks" path = "src/main.rs" [dependencies] -uucore = { workspace = true } -clap = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } +uucore = { workspace = true } +clap = { workspace = true } +libc = { workspace = true } +smartcols-sys = { workspace = true } +libmount-sys = { workspace = true } diff --git a/src/uu/lslocks/columns.json b/src/uu/lslocks/columns.json new file mode 100644 index 0000000..136ce93 --- /dev/null +++ b/src/uu/lslocks/columns.json @@ -0,0 +1,69 @@ +{ + "lslocks-columns": [ + { + "holder": "COMMAND", + "type": "", + "description": "command of the process holding the lock" + }, + { + "holder": "PID", + "type": "", + "description": "PID of the process holding the lock" + }, + { + "holder": "TYPE", + "type": "", + "description": "kind of lock" + }, + { + "holder": "SIZE", + "type": "", + "description": "size of the lock, use if --bytes is given" + }, + { + "holder": "INODE", + "type": "", + "description": "inode number" + }, + { + "holder": "MAJ:MIN", + "type": "", + "description": "major:minor device number" + }, + { + "holder": "MODE", + "type": "", + "description": "lock access mode" + }, + { + "holder": "M", + "type": "", + "description": "mandatory state of the lock: 0 (none), 1 (set)" + }, + { + "holder": "START", + "type": "", + "description": "relative byte offset of the lock" + }, + { + "holder": "END", + "type": "", + "description": "ending offset of the lock" + }, + { + "holder": "PATH", + "type": "", + "description": "path of the locked file" + }, + { + "holder": "BLOCKER", + "type": "", + "description": "PID of the process blocking the lock" + }, + { + "holder": "HOLDERS", + "type": "", + "description": "holders of the lock" + } + ] +} \ No newline at end of file diff --git a/src/uu/lslocks/columns.raw b/src/uu/lslocks/columns.raw new file mode 100644 index 0000000..3786baf --- /dev/null +++ b/src/uu/lslocks/columns.raw @@ -0,0 +1,13 @@ +COMMAND command\x20of\x20the\x20process\x20holding\x20the\x20lock +PID PID\x20of\x20the\x20process\x20holding\x20the\x20lock +TYPE kind\x20of\x20lock +SIZE size\x20of\x20the\x20lock,\x20use\x20\x20if\x20--bytes\x20is\x20given +INODE inode\x20number +MAJ:MIN major:minor\x20device\x20number +MODE lock\x20access\x20mode +M mandatory\x20state\x20of\x20the\x20lock:\x200\x20(none),\x201\x20(set) +START relative\x20byte\x20offset\x20of\x20the\x20lock +END ending\x20offset\x20of\x20the\x20lock +PATH path\x20of\x20the\x20locked\x20file +BLOCKER PID\x20of\x20the\x20process\x20blocking\x20the\x20lock +HOLDERS holders\x20of\x20the\x20lock diff --git a/src/uu/lslocks/columns.txt b/src/uu/lslocks/columns.txt new file mode 100644 index 0000000..4e87dab --- /dev/null +++ b/src/uu/lslocks/columns.txt @@ -0,0 +1,13 @@ +COMMAND command of the process holding the lock + PID PID of the process holding the lock + TYPE kind of lock + SIZE size of the lock, use if --bytes is given + INODE inode number +MAJ:MIN major:minor device number + MODE lock access mode + M mandatory state of the lock: 0 (none), 1 (set) + START relative byte offset of the lock + END ending offset of the lock + PATH path of the locked file +BLOCKER PID of the process blocking the lock +HOLDERS holders of the lock diff --git a/src/uu/lslocks/lslocks.md b/src/uu/lslocks/lslocks.md index 8982646..56ad5fd 100644 --- a/src/uu/lslocks/lslocks.md +++ b/src/uu/lslocks/lslocks.md @@ -1,7 +1,10 @@ # lslocks ``` -lslocks [OPTION]... +lslocks [-b|--bytes] [-i|--noinaccessible] [-J|--json|-r|--raw] [-n|--noheadings] [-o list|--output list] [--output-all] [-p pid|--pid pid] [-u|--notruncate] +lslocks {-H|--list-columns} [-J|--json|-r|--raw] +lslocks {-V|--version} +lslocks {-h|--help} ``` -lists system locks +list local system locks. diff --git a/src/uu/lslocks/src/column.rs b/src/uu/lslocks/src/column.rs new file mode 100644 index 0000000..222919d --- /dev/null +++ b/src/uu/lslocks/src/column.rs @@ -0,0 +1,122 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::ffi::{CStr, c_uint}; +use std::str::FromStr; + +use smartcols_sys::{ + SCOLS_FL_RIGHT, SCOLS_FL_TRUNC, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING, SCOLS_JSON_BOOLEAN, + SCOLS_JSON_NUMBER, SCOLS_JSON_STRING, +}; + +use crate::errors::LsLocksError; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct ColumnInfo { + pub(crate) id: &'static CStr, + pub(crate) width_hint: f64, + pub(crate) flags: c_uint, + pub(crate) default_json_type: c_uint, +} + +impl ColumnInfo { + const fn new( + id: &'static CStr, + width_hint: f64, + flags: c_uint, + default_json_type: c_uint, + ) -> Self { + Self { + id, + width_hint, + flags, + default_json_type, + } + } + + pub(crate) fn json_type(&self, in_bytes: bool) -> c_uint { + if in_bytes && self.id.to_bytes() == b"SIZE" { + SCOLS_JSON_NUMBER + } else { + self.default_json_type + } + } +} + +pub(crate) static COLUMN_INFOS: [ColumnInfo; 13] = [ + ColumnInfo::new(c"COMMAND", 15.0, 0, SCOLS_JSON_STRING), + ColumnInfo::new(c"PID", 5.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER), + ColumnInfo::new(c"TYPE", 5.0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING), + ColumnInfo::new(c"SIZE", 4.0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING), + ColumnInfo::new(c"INODE", 5.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER), + ColumnInfo::new(c"MAJ:MIN", 6.0, 0, SCOLS_JSON_STRING), + ColumnInfo::new(c"MODE", 5.0, 0, SCOLS_JSON_STRING), + ColumnInfo::new(c"M", 1.0, 0, SCOLS_JSON_BOOLEAN), + ColumnInfo::new(c"START", 10.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER), + ColumnInfo::new(c"END", 10.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER), + ColumnInfo::new(c"PATH", 0.0, SCOLS_FL_TRUNC, SCOLS_JSON_STRING), + ColumnInfo::new(c"BLOCKER", 0.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER), + ColumnInfo::new(c"HOLDERS", 0.0, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING), +]; + +pub(crate) static ALL: [&str; 13] = [ + "COMMAND", "PID", "TYPE", "SIZE", "INODE", "MAJ:MIN", "MODE", "M", "START", "END", "PATH", + "BLOCKER", "HOLDERS", +]; + +pub(crate) static DEFAULT: [&str; 9] = [ + "COMMAND", "PID", "TYPE", "SIZE", "MODE", "M", "START", "END", "PATH", +]; + +#[derive(Debug, Clone)] +pub(crate) struct OutputColumns { + pub(crate) append: bool, + pub(crate) list: Vec<&'static ColumnInfo>, +} + +impl Default for OutputColumns { + fn default() -> Self { + Self { + append: true, + list: Vec::default(), + } + } +} + +impl FromStr for OutputColumns { + type Err = LsLocksError; + + fn from_str(s: &str) -> Result { + let suffix = s.strip_prefix('+'); + let append = suffix.is_some(); + + let list: Vec<_> = suffix + .unwrap_or(s) + .split(',') + .map(|name| { + COLUMN_INFOS + .iter() + .find(|&column| column.id.to_str().unwrap() == name) + .ok_or_else(|| LsLocksError::InvalidColumnName(name.into())) + }) + .collect::>()?; + + if list.is_empty() { + Err(LsLocksError::InvalidColumnSequence(s.into())) + } else { + Ok(Self { append, list }) + } + } +} + +impl From<&'_ clap::ArgMatches> for OutputColumns { + fn from(args: &clap::ArgMatches) -> Self { + args.get_one::(crate::options::OUTPUT) + .map_or_else(Self::default, |columns| Self { + append: columns.append, + list: columns.list.clone(), + }) + } +} diff --git a/src/uu/lslocks/src/display.rs b/src/uu/lslocks/src/display.rs new file mode 100644 index 0000000..92f23da --- /dev/null +++ b/src/uu/lslocks/src/display.rs @@ -0,0 +1,136 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::borrow::Cow; +use std::ffi::{CStr, CString}; +use std::sync::atomic::AtomicPtr; +use std::{io, ptr}; + +use crate::errors::LsLocksError; +use crate::utils::LockInfo; + +fn decimal_point() -> &'static str { + use std::sync::atomic::Ordering; + + static DEFAULT: &CStr = c"."; + static VALUE: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + + let mut decimal_point = VALUE.load(Ordering::Acquire); + + if decimal_point.is_null() { + decimal_point = unsafe { libc::localeconv().as_ref() } + .and_then(|lc| (!lc.decimal_point.is_null()).then_some(lc.decimal_point)) + .unwrap_or(DEFAULT.as_ptr().cast_mut()) + .cast(); + + match VALUE.compare_exchange( + ptr::null_mut(), + decimal_point, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_previous_value) => {} + Err(previous_value) => decimal_point = previous_value, + } + } + + unsafe { CStr::from_ptr(decimal_point.cast()) } + .to_str() + .unwrap() +} + +// returns exponent (2^x=n) in range KiB..EiB (2^10..2^60). +fn bytes_exponent(bytes: u64) -> u64 { + for shift in (10..=60).step_by(10) { + if bytes < (1 << shift) { + return shift - 10; + } + } + 60 +} + +fn size_to_human_string(bytes: u64) -> String { + static LETTERS: [char; 7] = ['B', 'K', 'M', 'G', 'T', 'P', 'E']; + + let exp = bytes_exponent(bytes); + let unit = LETTERS[if exp == 0 { 0 } else { (exp / 10) as usize }]; + let mut decimal = if exp == 0 { bytes } else { bytes / (1 << exp) }; + let mut fractional = if exp == 0 { 0 } else { bytes % (1 << exp) }; + + if fractional != 0 { + fractional = if fractional >= (u64::MAX / 1000) { + ((fractional / 1024) * 1000) / (1 << (exp - 10)) + } else { + (fractional * 1000) / (1 << exp) + }; + + fractional = ((fractional + 50) / 100) * 10; + + if fractional == 100 { + decimal += 1; + fractional = 0; + } + } + + if fractional == 0 { + format!("{decimal}{unit}") + } else { + format!("{decimal}{}{fractional:02}{unit}", decimal_point()) + } +} + +pub(crate) fn describe_integer(n: T) -> Option> { + Some(Cow::Owned(CString::new(n.to_string()).unwrap())) +} + +pub(crate) fn describe_size(size: u64, in_bytes: bool) -> Option> { + let value = if in_bytes { + size.to_string() + } else { + size_to_human_string(size) + }; + + Some(Cow::Owned(CString::new(value).unwrap())) +} + +pub(crate) fn describe_holders( + proc_lock: &LockInfo, + pid_locks: &[LockInfo], +) -> Result { + let lock_compare = move |lock: &&LockInfo| { + lock.range == proc_lock.range + && lock.inode == proc_lock.inode + && lock.device_id == proc_lock.device_id + && lock.mandatory == proc_lock.mandatory + && lock.blocked == proc_lock.blocked + && lock.kind == proc_lock.kind + && lock.mode == proc_lock.mode + }; + + let mut separator: &[u8] = &[]; + + let append_holder = move |mut buffer: Vec, lock: &LockInfo| { + buffer.extend(separator); + separator = b"\n"; + + buffer.extend(lock.process_id.to_string().into_bytes()); + buffer.push(b','); + + if let Some(command_line) = lock.command_name.as_deref().map(CStr::to_bytes) { + buffer.extend(command_line); + } + + buffer.push(b','); + buffer.extend(lock.file_descriptor.to_string().into_bytes()); + buffer + }; + + let buffer = pid_locks + .iter() + .filter(lock_compare) + .fold(Vec::default(), append_holder); + + CString::new(buffer).map_err(|_| LsLocksError::io0("invalid data", io::ErrorKind::InvalidData)) +} diff --git a/src/uu/lslocks/src/errors.rs b/src/uu/lslocks/src/errors.rs new file mode 100644 index 0000000..fb14c66 --- /dev/null +++ b/src/uu/lslocks/src/errors.rs @@ -0,0 +1,67 @@ +// This file is part of the uutils hostname package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::ffi::c_int; +use std::fmt; +use std::path::PathBuf; + +use uucore::error::UError; + +#[derive(Debug)] +pub enum LsLocksError { + InvalidColumnName(String), + InvalidColumnSequence(String), + IO0(String, std::io::Error), + IO1(String, PathBuf, std::io::Error), +} + +impl LsLocksError { + pub(crate) fn io0(message: impl Into, error: impl Into) -> Self { + Self::IO0(message.into(), error.into()) + } + + pub(crate) fn io1( + message: impl Into, + path: impl Into, + error: impl Into, + ) -> Self { + Self::IO1(message.into(), path.into(), error.into()) + } + + pub(crate) fn io_from_neg_errno( + message: impl Into, + result: c_int, + ) -> Result { + if let Ok(result) = usize::try_from(result) { + Ok(result) + } else { + let err = std::io::Error::from_raw_os_error(-result); + Err(Self::IO0(message.into(), err)) + } + } +} + +impl fmt::Display for LsLocksError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IO0(message, err) => write!(f, "{message}: {err}"), + Self::IO1(message, path, err) => write!(f, "{message} '{}': {err}", path.display()), + Self::InvalidColumnName(name) => write!(f, "invalid column name: {name}"), + Self::InvalidColumnSequence(seq) => write!(f, "invalid column sequence: {seq}"), + } + } +} + +impl UError for LsLocksError { + fn code(&self) -> i32 { + 1 + } + + fn usage(&self) -> bool { + false + } +} + +impl std::error::Error for LsLocksError {} diff --git a/src/uu/lslocks/src/lslocks.rs b/src/uu/lslocks/src/lslocks.rs index 129d6cb..455bb36 100644 --- a/src/uu/lslocks/src/lslocks.rs +++ b/src/uu/lslocks/src/lslocks.rs @@ -3,295 +3,61 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::{fmt, fs, str::FromStr}; +// Remove this if the tool is ported to Non-UNIX platforms. +#![cfg_attr( + not(target_os = "linux"), + allow(dead_code, non_camel_case_types, unused_imports) +)] -use clap::{crate_version, Command}; +#[cfg(target_os = "linux")] +mod column; +#[cfg(target_os = "linux")] +mod display; +mod errors; +#[cfg(target_os = "linux")] +mod smartcols; +#[cfg(target_os = "linux")] +mod utils; + +use std::borrow::Cow; +use std::ffi::{CStr, CString, OsStr}; +use std::io::Write; +use std::path::Path; +use std::ptr; +use std::str::FromStr; + +use clap::{Arg, ArgAction, ArgMatches, Command, crate_version, value_parser}; use uucore::{error::UResult, format_usage, help_about, help_usage}; -// See https://www.man7.org/linux/man-pages/man5/proc_locks.5.html for details on each field's meaning -#[derive(Debug)] -struct Lock { - _ord: usize, - lock_type: LockType, - mandatory: bool, - mode: LockMode, - pid: Option, // This value is -1 for OFD locks, hence the Option - major_minor: String, - inode: usize, - start_offset: usize, // Byte offset to start of lock - end_offset: Option, // None = lock does not have an explicit end offset and applies until the end of the file -} +#[cfg(target_os = "linux")] +use crate::column::{ColumnInfo, OutputColumns}; +#[cfg(target_os = "linux")] +use crate::display::{describe_holders, describe_integer, describe_size}; +use crate::errors::LsLocksError; +#[cfg(target_os = "linux")] +use crate::smartcols::{Table, TableOperations}; +#[cfg(target_os = "linux")] +use crate::utils::{ + _PATH_PROC, _PATH_PROC_LOCKS, BinFileLineIter, LockInfo, entry_is_dir_or_unknown, + proc_pid_command_name, +}; +#[cfg(target_os = "linux")] +use libc::pid_t; +#[cfg(not(target_os = "linux"))] +type pid_t = i32; -impl Lock { - fn get_value(&self, col: &Column) -> String { - match col { - Column::Command => resolve_command(self).unwrap_or("".to_string()), - Column::Pid => self - .pid - .map(|pid| pid.to_string()) - .unwrap_or("-".to_string()), - Column::Type => self.lock_type.to_string(), - Column::Size => todo!(), - Column::Inode => self.inode.to_string(), - Column::MajorMinor => self.major_minor.clone(), - Column::Mode => self.mode.to_string(), - Column::Mandatory => { - if self.mandatory { - "1".to_string() - } else { - "0".to_string() - } - } - Column::Start => self.start_offset.to_string(), - // TODO: In the case of EOF end_offset, we should actually resolve the actual file size and display that as the end offset - Column::End => self - .end_offset - .map(|offset| offset.to_string()) - .unwrap_or("EOF".to_string()), - Column::Path => todo!(), // TODO: Resolve filepath of the lock target - Column::Blocker => todo!(), // TODO: Check if lock is blocker (and by what) - Column::Holders => todo!(), // TODO: Resolve all holders of the lock (this would also let us display a process/PID for OFD locks) - } - } -} - -impl FromStr for Lock { - type Err = (); - fn from_str(input: &str) -> Result { - let mut parts = input.split_whitespace(); - - // Ordinal position comes in the form of `:`, so we need to strip away the `:` - let ord = parts - .next() - .and_then(|s| s.strip_suffix(":")) - .unwrap() - .parse::() - .unwrap(); - let lock_type = parts - .next() - .and_then(|part| LockType::from_str(part).ok()) - .unwrap(); - let mandatory = parts - .next() - .map(|part| match part { - "MANDATORY" => true, - "ADVISORY" => false, - _ => panic!("Unrecognized value in lock line: {}", part), - }) - .unwrap(); - let mode = parts - .next() - .and_then(|part| LockMode::from_str(part).ok()) - .unwrap(); - let pid: Option = parts.next().and_then(|pid_str| match pid_str { - "-1" => None, - other => other.parse::().ok(), - }); - - if lock_type == LockType::OFDLCK && pid.is_some() { - println!("Unexpected PID value on OFD lock: '{}'", input); - return Err(()); - }; - - // This field has a format of MAJOR:MINOR:INODE - let major_minor_inode: Vec<_> = parts.next().unwrap().split(":").collect(); - assert_eq!(major_minor_inode.len(), 3); - let major_minor = [major_minor_inode[0], major_minor_inode[1]].join(":"); - let inode = major_minor_inode[2].parse::().unwrap(); - - let start_offset = parts.next().unwrap().parse::().unwrap(); - let end_offset: Option = parts.next().and_then(|offset_str| match offset_str { - "EOF" => None, - other => other.parse::().ok(), - }); - - Ok(Self { - _ord: ord, - lock_type, - mandatory, - mode, - pid, - major_minor, - inode, - start_offset, - end_offset, - }) - } -} - -#[derive(Debug, PartialEq, Eq)] -#[allow(clippy::upper_case_acronyms)] -enum LockType { - FLOCK, // BSD file lock - OFDLCK, // Open file descriptor - POSIX, // POSIX byte-range lock -} - -impl FromStr for LockType { - type Err = (); - fn from_str(input: &str) -> Result { - match input { - "FLOCK" => Ok(Self::FLOCK), - "OFDLCK" => Ok(Self::OFDLCK), - "POSIX" => Ok(Self::POSIX), - _ => Err(()), - } - } -} - -impl fmt::Display for LockType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LockType::FLOCK => write!(f, "FLOCK"), - LockType::OFDLCK => write!(f, "OFDLCK"), - LockType::POSIX => write!(f, "POSIX"), - } - } -} - -#[derive(Debug)] -enum LockMode { - Read, - Write, -} - -impl FromStr for LockMode { - type Err = (); - fn from_str(input: &str) -> Result { - match input { - "WRITE" => Ok(Self::Write), - "READ" => Ok(Self::Read), - _ => Err(()), - } - } -} - -impl fmt::Display for LockMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LockMode::Write => write!(f, "WRITE"), - LockMode::Read => write!(f, "READ"), - } - } -} - -// All of the columns that need to be supported in the final version -#[derive(Clone)] -#[allow(dead_code)] -enum Column { - Command, - Pid, - Type, - Size, - Inode, - MajorMinor, - Mode, - Mandatory, - Start, - End, - Path, - Blocker, - Holders, -} - -impl Column { - fn header_text(&self) -> &'static str { - match self { - Self::Command => "COMMAND", - Self::Pid => "PID", - Self::Type => "TYPE", - Self::Size => "SIZE", - Self::Inode => "INODE", - Self::MajorMinor => "MAJ:MIN", - Self::Mode => "MODE", - Self::Mandatory => "M", - Self::Start => "START", - Self::End => "END", - Self::Path => "PATH", - Self::Blocker => "BLOCKER", - Self::Holders => "HOLDERS", - } - } -} - -fn resolve_command(lock: &Lock) -> Option { - if let Some(pid) = lock.pid { - return fs::read_to_string(format!("/proc/{}/comm", pid)) - .map(|content| content.trim().to_string()) - .ok(); - } - - // File descriptor locks don't have a real notion of an "owner process", since it can be shared by multiple processes. - // The original `lslocks` goes through `/proc//fdinfo/*` to find *any* of the processes that reference the same device/inode combo from the lock - // We don't implement this behaviour yet, and just show "unknown" for the locks which don't have a clear owner - None -} - -const DEFAULT_COLS: &[Column] = &[ - Column::Command, - Column::Pid, - Column::Type, - //TODO: Implement Column::Size here - Column::Mode, - Column::Mandatory, - Column::Start, - Column::End, - //TODO: Implements Column::Path here -]; - -struct OutputOptions { - cols: Vec, -} - -fn print_output(locks: Vec, output_opts: OutputOptions) { - let mut column_widths: Vec<_> = output_opts - .cols - .iter() - .map(|col| col.header_text().len()) - .collect(); - - for lock in &locks { - for (i, col) in output_opts.cols.iter().enumerate() { - column_widths[i] = column_widths[i].max(lock.get_value(col).len()); - } - } - - let headers: Vec<_> = output_opts - .cols - .iter() - .enumerate() - .map(|(i, col)| format!("{: = output_opts - .cols - .iter() - .enumerate() - .map(|(i, col)| format!("{: UResult<()> { - let output_opts = OutputOptions { - cols: Vec::from(DEFAULT_COLS), - }; - - let locks: Vec<_> = match fs::read_to_string("/proc/locks") { - Ok(content) => content - .lines() - .map(|line| Lock::from_str(line).unwrap()) - .collect(), - Err(e) => panic!("Could not read /proc/locks: {}", e), - }; - - print_output(locks, output_opts); - Ok(()) +mod options { + pub static BYTES: &str = "bytes"; + pub static JSON: &str = "json"; + pub static LIST_COLUMNS: &str = "list-columns"; + pub static LIST: &str = "list"; + pub static NO_HEADINGS: &str = "noheadings"; + pub static NO_INACCESSIBLE: &str = "noinaccessible"; + pub static NO_TRUNCATE: &str = "notruncate"; + pub static OUTPUT_ALL: &str = "output-all"; + pub static OUTPUT: &str = "output"; + pub static PID: &str = "pid"; + pub static RAW: &str = "raw"; } const ABOUT: &str = help_about!("lslocks.md"); @@ -303,4 +69,447 @@ pub fn uu_app() -> Command { .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .arg( + Arg::new(options::BYTES) + .short('b') + .long(options::BYTES) + .action(ArgAction::SetTrue) + .help("print SIZE in bytes rather than in human readable format"), + ) + .arg( + Arg::new(options::NO_INACCESSIBLE) + .short('i') + .long(options::NO_INACCESSIBLE) + .action(ArgAction::SetTrue) + .help("ignore locks without read permissions"), + ) + .arg( + Arg::new(options::JSON) + .short('J') + .long(options::JSON) + .action(ArgAction::SetTrue) + .conflicts_with(options::RAW) + .help("use JSON output format"), + ) + .arg( + Arg::new(options::LIST_COLUMNS) + .short('H') + .long(options::LIST_COLUMNS) + .action(ArgAction::SetTrue) + .conflicts_with_all([ + options::BYTES, + options::NO_INACCESSIBLE, + options::NO_HEADINGS, + options::OUTPUT, + options::OUTPUT_ALL, + options::PID, + options::NO_TRUNCATE, + ]) + .help("list the available columns"), + ) + .arg( + Arg::new(options::NO_HEADINGS) + .short('n') + .long(options::NO_HEADINGS) + .action(ArgAction::SetTrue) + .help("don't print headings"), + ) + .arg( + Arg::new(options::OUTPUT) + .short('o') + .long(options::OUTPUT) + .value_name(options::LIST) + .value_parser(OutputColumns::from_str) + .action(ArgAction::Set) + .help("output columns (see --list-columns)"), + ) + .arg( + Arg::new(options::OUTPUT_ALL) + .long(options::OUTPUT_ALL) + .action(ArgAction::SetTrue) + .help("output all columns"), + ) + .arg( + Arg::new(options::PID) + .short('p') + .long(options::PID) + .value_name(options::PID) + .value_parser(value_parser!(pid_t)) + .action(ArgAction::Set) + .help("display only locks held by this process"), + ) + .arg( + Arg::new(options::NO_TRUNCATE) + .short('u') + .long(options::NO_TRUNCATE) + .action(ArgAction::SetTrue) + .help("don't truncate text in columns"), + ) + .arg( + Arg::new(options::RAW) + .short('r') + .long(options::RAW) + .action(ArgAction::SetTrue) + .conflicts_with(options::JSON) + .help("use the raw output format"), + ) +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = uu_app().try_get_matches_from_mut(args)?; + + let output_mode = OutputMode::from(&args); + + if args.get_flag(options::LIST_COLUMNS) { + list_columns(output_mode)?; + return Ok(()); + } + + lslocks(&args, output_mode).map_err(From::from) +} + +// The Linux implementation resides in `crate::column`. +#[cfg(not(target_os = "linux"))] +#[derive(Debug, Clone)] +struct OutputColumns; + +#[cfg(not(target_os = "linux"))] +impl FromStr for OutputColumns { + type Err = LsLocksError; + + fn from_str(_s: &str) -> Result { + unimplemented!() + } +} + +#[cfg(target_os = "linux")] +fn lslocks(args: &ArgMatches, output_mode: OutputMode) -> Result<(), LsLocksError> { + let columns = collect_output_columns(args)?; + + let in_bytes = args.get_flag(options::BYTES); + let mut table = setup_table(args, output_mode, in_bytes, &columns)?; + + let no_inaccessible = args.get_flag(options::NO_INACCESSIBLE); + let pid_locks = collect_pid_locks(no_inaccessible)?; + + let path = Path::new(_PATH_PROC_LOCKS); + let mut lines = BinFileLineIter::open(path)?; + + let mut proc_locks = Vec::default(); + + while let Some(line) = lines.next_line()? { + if let Some(lock) = + LockInfo::parse(no_inaccessible, path, None, -1, c"", Some(&pid_locks), line)? + { + proc_locks.push(lock); + } + } + + let target_pid = args.get_one::(options::PID).copied(); + + fill_table( + output_mode, + &columns, + target_pid, + in_bytes, + &mut table, + &pid_locks, + &proc_locks, + )?; + + table.print() +} + +#[cfg(not(target_os = "linux"))] +fn lslocks(_args: &ArgMatches, _output_mode: OutputMode) -> Result<(), LsLocksError> { + unimplemented!() +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +pub(crate) enum OutputMode { + None = 0, + Raw, + Json, +} + +impl From<&'_ ArgMatches> for OutputMode { + fn from(args: &ArgMatches) -> Self { + if args.get_flag(options::JSON) { + Self::Json + } else if args.get_flag(options::RAW) { + Self::Raw + } else { + Self::None + } + } +} + +fn list_columns(output_mode: OutputMode) -> Result<(), LsLocksError> { + let mut stdout = std::io::stdout().lock(); + + match output_mode { + OutputMode::None => stdout.write_all(include_bytes!("../columns.txt")), + OutputMode::Raw => stdout.write_all(include_bytes!("../columns.raw")), + OutputMode::Json => stdout.write_all(include_bytes!("../columns.json")), + } + .map_err(|err| LsLocksError::io0("stdout", err)) +} + +#[cfg(target_os = "linux")] +fn collect_output_columns(args: &ArgMatches) -> Result, LsLocksError> { + let columns = OutputColumns::from(args); + + let columns = if columns.append { + let default_names: &[&str] = if args.get_flag(options::OUTPUT_ALL) { + &column::ALL + } else { + &column::DEFAULT + }; + + let mut default_columns = default_names + .iter() + .map(|&name| { + column::COLUMN_INFOS + .iter() + .find(|&column| column.id.to_str().unwrap() == name) + .ok_or_else(|| LsLocksError::InvalidColumnName(name.into())) + }) + .collect::, _>>()?; + + default_columns.extend(columns.list); + default_columns + } else { + columns.list + }; + + Ok(columns) +} + +#[cfg(target_os = "linux")] +fn setup_table( + args: &ArgMatches, + output_mode: OutputMode, + in_bytes: bool, + columns: &[&ColumnInfo], +) -> Result { + use smartcols_sys::{ + SCOLS_FL_TRUNC, SCOLS_FL_WRAP, scols_wrapnl_chunksize, scols_wrapnl_nextchunk, + }; + + smartcols::initialize(); + + let mut table = Table::new()?; + + if args.get_flag(options::JSON) { + table.set_name(c"locks")?; + } + + if args.get_flag(options::NO_HEADINGS) { + table.enable_headings(false)?; + } + + match output_mode { + OutputMode::Raw => table.enable_raw(true)?, + OutputMode::Json => table.enable_json(true)?, + OutputMode::None => {} + } + + let no_truncate = args.get_flag(options::NO_TRUNCATE); + + for &column_info in columns { + let mut flags = column_info.flags; + if no_truncate { + flags &= !SCOLS_FL_TRUNC; + } + let mut column = table.new_column(column_info.id, column_info.width_hint, flags)?; + + if (flags & SCOLS_FL_WRAP) != 0 { + column.set_wrap_func( + Some(scols_wrapnl_chunksize), + Some(scols_wrapnl_nextchunk), + ptr::null_mut(), + )?; + + column.set_safe_chars(c"\n")?; + } + + if output_mode == OutputMode::Json { + column.set_json_type(column_info.json_type(in_bytes))?; + } + } + + Ok(table) +} + +#[cfg(target_os = "linux")] +fn collect_pid_locks(no_inaccessible: bool) -> Result, LsLocksError> { + let path = Path::new(_PATH_PROC); + let dir_entries = path + .read_dir() + .map_err(|err| LsLocksError::io1("reading directory", path, err))?; + + let mut pid_locks = Vec::default(); + + for entry in dir_entries { + let entry = entry.map_err(|err| LsLocksError::io1("reading directory entry", path, err))?; + + let Some(process_id) = entry.file_name().to_str().and_then(|s| s.parse().ok()) else { + continue; + }; + + let path = entry.path(); + let file_type = entry + .file_type() + .map_err(|err| LsLocksError::io1("reading directory entry type", &path, err))?; + + if !entry_is_dir_or_unknown(&file_type) { + continue; + } + + let command_name = proc_pid_command_name(&path); + + // We should report the error instead of silently continuing. + let _ignored = append_pid_locks( + no_inaccessible, + &path.join("fdinfo"), + process_id, + command_name.as_deref().unwrap_or(c""), + &mut pid_locks, + ); + } + + Ok(pid_locks) +} + +#[cfg(target_os = "linux")] +fn append_pid_locks( + no_inaccessible: bool, + path: &Path, + process_id: pid_t, + command_name: &CStr, + pid_locks: &mut Vec, +) -> Result<(), LsLocksError> { + let Ok(dir_entries) = path.read_dir() else { + //eprintln!("{}", LsLocksError::io1("reading directory", &path, err)); + return Ok(()); // We should report the error instead of silently continuing. + }; + + for entry in dir_entries { + let entry = entry.map_err(|err| LsLocksError::io1("reading directory entry", path, err))?; + + let Some(file_descriptor) = entry.file_name().to_str().and_then(|s| s.parse().ok()) else { + continue; + }; + + let path = entry.path(); + let mut lines = BinFileLineIter::open(&path)?; + + // This silently ignores potential errors that prevent us from reading the next line. + // We should report the error instead of silently ignoring it. + while let Ok(Some(line)) = lines.next_line() { + let Some(suffix) = line.strip_prefix(b"lock:").map(<[u8]>::trim_ascii) else { + continue; + }; + + if let Some(lock) = LockInfo::parse( + no_inaccessible, + &path, + Some(process_id), + file_descriptor, + command_name, + None, + suffix, + )? { + pid_locks.push(lock); + } + } + } + Ok(()) +} + +#[cfg(target_os = "linux")] +fn fill_table( + output_mode: OutputMode, + columns: &[&ColumnInfo], + target_pid: Option, + in_bytes: bool, + table: &mut Table, + pid_locks: &[LockInfo], + proc_locks: &[LockInfo], +) -> Result<(), LsLocksError> { + use std::os::unix::ffi::OsStrExt; + + for proc_lock in proc_locks + .iter() + .rev() + .filter(|lock| target_pid.is_none_or(|target_pid| lock.process_id == target_pid)) + { + let mut line = table.new_line(None)?; + + for (cell_index, &column) in columns.iter().enumerate() { + let data_str = match column.id.to_bytes() { + b"PID" => describe_integer(proc_lock.process_id), + b"INODE" => describe_integer(proc_lock.inode), + b"M" => describe_integer(u8::from(proc_lock.mandatory)), + b"START" => describe_integer(proc_lock.range.start), + b"END" => describe_integer(proc_lock.range.end), + + b"SIZE" => proc_lock + .size + .and_then(|size| describe_size(size, in_bytes)), + + b"TYPE" => Some(Cow::Borrowed(proc_lock.kind.as_c_str())), + + b"MAJ:MIN" => { + let major = libc::major(proc_lock.device_id); + let minor = libc::minor(proc_lock.device_id); + let value = if matches!(output_mode, OutputMode::Json | OutputMode::Raw) { + format!("{major}:{minor}") + } else { + format!("{major:3}:{minor:<3}") + }; + Some(Cow::Owned(CString::new(value).unwrap())) + } + + b"MODE" => { + if proc_lock.blocked { + let mut buffer = proc_lock.mode.clone().into_bytes(); + buffer.push(b'*'); + Some(Cow::Owned(CString::new(buffer).unwrap())) + } else { + Some(Cow::Borrowed(proc_lock.mode.as_c_str())) + } + } + + b"COMMAND" => proc_lock.command_name.as_deref().map(Cow::Borrowed), + + b"PATH" => proc_lock + .path + .as_deref() + .map(Path::as_os_str) + .map(OsStr::as_bytes) + .map(|path| Cow::Owned(CString::new(path).unwrap())), + + b"BLOCKER" => (proc_lock.blocked && proc_lock.id != -1) + .then(|| proc_locks.iter()) + .and_then(|mut iter| { + iter.find(|&lock| !lock.blocked && lock.id == proc_lock.id) + }) + .and_then(|found| describe_integer(found.process_id)), + + b"HOLDERS" => describe_holders(proc_lock, pid_locks) + .map(Cow::Owned) + .map(Some)?, + + _ => continue, + }; + + if let Some(data_str) = data_str { + line.set_data(cell_index, &data_str)?; + } + } + } + Ok(()) } diff --git a/src/uu/lslocks/src/smartcols.rs b/src/uu/lslocks/src/smartcols.rs new file mode 100644 index 0000000..01075c3 --- /dev/null +++ b/src/uu/lslocks/src/smartcols.rs @@ -0,0 +1,137 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::ffi::{CStr, c_char, c_int, c_uint, c_void}; +use std::ptr::NonNull; +use std::{io, ptr}; + +use smartcols_sys::{ + libscols_column, libscols_line, libscols_table, scols_column_set_json_type, + scols_column_set_safechars, scols_column_set_wrapfunc, scols_init_debug, scols_line_set_data, + scols_new_table, scols_print_table, scols_table_enable_json, scols_table_enable_noheadings, + scols_table_enable_raw, scols_table_new_column, scols_table_new_line, scols_table_set_name, + scols_unref_table, +}; + +use crate::errors::LsLocksError; + +pub(crate) fn initialize() { + unsafe { scols_init_debug(0) }; +} + +#[repr(transparent)] +pub(crate) struct Table(NonNull); + +impl Table { + pub(crate) fn new() -> Result { + NonNull::new(unsafe { scols_new_table() }) + .ok_or_else(|| LsLocksError::io0("scols_new_table", io::ErrorKind::OutOfMemory)) + .map(Self) + } +} + +impl TableOperations for Table { + fn as_ptr(&self) -> *mut libscols_table { + self.0.as_ptr() + } +} + +impl Drop for Table { + fn drop(&mut self) { + unsafe { scols_unref_table(self.0.as_ptr()) } + } +} + +pub(crate) trait TableOperations: Sized { + fn as_ptr(&self) -> *mut libscols_table; + + fn enable_headings(&mut self, enable: bool) -> Result<(), LsLocksError> { + let no_headings = c_int::from(!enable); + let r = unsafe { scols_table_enable_noheadings(self.as_ptr(), no_headings) }; + LsLocksError::io_from_neg_errno("scols_table_enable_noheadings", r).map(|_| ()) + } + + fn enable_raw(&mut self, enable: bool) -> Result<(), LsLocksError> { + let r = unsafe { scols_table_enable_raw(self.as_ptr(), c_int::from(enable)) }; + LsLocksError::io_from_neg_errno("scols_table_enable_raw", r).map(|_| ()) + } + + fn enable_json(&mut self, enable: bool) -> Result<(), LsLocksError> { + let r = unsafe { scols_table_enable_json(self.as_ptr(), c_int::from(enable)) }; + LsLocksError::io_from_neg_errno("scols_table_enable_json", r).map(|_| ()) + } + + fn new_column( + &mut self, + name: &CStr, + width_hint: f64, + flags: c_uint, + ) -> Result { + NonNull::new(unsafe { + scols_table_new_column(self.as_ptr(), name.as_ptr(), width_hint, flags as c_int) + }) + .ok_or_else(|| LsLocksError::io0("scols_table_new_column", io::ErrorKind::OutOfMemory)) + .map(ColumnRef) + } + + fn new_line(&mut self, parent: Option<&mut LineRef>) -> Result { + let parent = parent.map_or(ptr::null_mut(), |parent| parent.0.as_ptr()); + + NonNull::new(unsafe { scols_table_new_line(self.as_ptr(), parent) }) + .ok_or_else(|| LsLocksError::io0("scols_table_new_line", io::ErrorKind::OutOfMemory)) + .map(LineRef) + } + + fn set_name(&mut self, name: &CStr) -> Result<(), LsLocksError> { + let r = unsafe { scols_table_set_name(self.as_ptr(), name.as_ptr()) }; + LsLocksError::io_from_neg_errno("scols_table_set_name", r).map(|_| ()) + } + + fn print(&self) -> Result<(), LsLocksError> { + let r = unsafe { scols_print_table(self.as_ptr()) }; + LsLocksError::io_from_neg_errno("scols_print_table", r).map(|_| ()) + } +} + +#[repr(transparent)] +pub(crate) struct LineRef(NonNull); + +impl LineRef { + pub(crate) fn set_data(&mut self, cell_index: usize, data: &CStr) -> Result<(), LsLocksError> { + let r = unsafe { scols_line_set_data(self.0.as_ptr(), cell_index, data.as_ptr()) }; + LsLocksError::io_from_neg_errno("scols_line_set_data", r).map(|_| ()) + } +} + +#[repr(transparent)] +pub(crate) struct ColumnRef(NonNull); + +impl ColumnRef { + pub(crate) fn set_json_type(&mut self, json_type: c_uint) -> Result<(), LsLocksError> { + let r = unsafe { scols_column_set_json_type(self.0.as_ptr(), json_type as c_int) }; + LsLocksError::io_from_neg_errno("scols_column_set_json_type", r).map(|_| ()) + } + + pub(crate) fn set_safe_chars(&mut self, safe: &CStr) -> Result<(), LsLocksError> { + let r = unsafe { scols_column_set_safechars(self.0.as_ptr(), safe.as_ptr()) }; + LsLocksError::io_from_neg_errno("scols_column_set_safechars", r).map(|_| ()) + } + + pub(crate) fn set_wrap_func( + &mut self, + wrap_chunk_size: Option< + unsafe extern "C" fn(*const libscols_column, *const c_char, *mut c_void) -> usize, + >, + wrap_next_chunk: Option< + unsafe extern "C" fn(*const libscols_column, *mut c_char, *mut c_void) -> *mut c_char, + >, + user_data: *mut c_void, + ) -> Result<(), LsLocksError> { + let r = unsafe { + scols_column_set_wrapfunc(self.0.as_ptr(), wrap_chunk_size, wrap_next_chunk, user_data) + }; + LsLocksError::io_from_neg_errno("scols_column_set_wrapfunc", r).map(|_| ()) + } +} diff --git a/src/uu/lslocks/src/utils.rs b/src/uu/lslocks/src/utils.rs new file mode 100644 index 0000000..0645204 --- /dev/null +++ b/src/uu/lslocks/src/utils.rs @@ -0,0 +1,365 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::ffi::{CStr, CString, c_int, c_uint}; +use std::fs::{File, FileType}; +use std::io; +use std::io::{BufRead, BufReader}; +use std::ops::Range; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; +use std::path::{Path, PathBuf}; + +use libmount_sys::{ + MNT_ITER_BACKWARD, mnt_fs_get_target, mnt_new_table_from_file, mnt_table_find_devno, +}; + +use crate::errors::LsLocksError; + +pub(crate) static _PATH_PROC: &str = "/proc"; +pub(crate) static _PATH_PROC_LOCKS: &str = "/proc/locks"; +static _PATH_PROC_MOUNTINFO: &str = "/proc/self/mountinfo"; +static _PATH_PROC_MOUNTINFO_C: &CStr = c"/proc/self/mountinfo"; + +pub(crate) fn entry_is_dir_or_unknown(file_type: &FileType) -> bool { + file_type.is_dir() + || (!file_type.is_file() + && !file_type.is_symlink() + && !file_type.is_block_device() + && !file_type.is_char_device() + && !file_type.is_fifo() + && !file_type.is_socket()) +} + +pub(crate) fn proc_pid_command_name(proc_path: &Path) -> Result { + let path = proc_path.join("comm"); + + let mut contents = + std::fs::read(&path).map_err(|err| LsLocksError::io1("reading file", &path, err))?; + if contents.last().copied() == Some(0_u8) { + contents.pop(); + } + if contents.last().copied() == Some(b'\n') { + contents.pop(); + } + + CString::new(contents).map_err(|_| { + let err = io::ErrorKind::InvalidData; + LsLocksError::io1("invalid data", &path, err) + }) +} + +fn pid_command_name(pid: libc::pid_t) -> Result { + proc_pid_command_name(&Path::new(_PATH_PROC).join(pid.to_string())) +} + +fn path_and_size_of_inode_opened_by_process( + pid: libc::pid_t, + inode: libc::ino_t, +) -> Result<(PathBuf, u64), LsLocksError> { + let path = Path::new(_PATH_PROC).join(format!("{pid}")).join("fd"); + + let dir_entries = path + .read_dir() + .map_err(|err| LsLocksError::io1("reading directory", &path, err))?; + + for entry in dir_entries { + let entry = + entry.map_err(|err| LsLocksError::io1("reading directory entry", &path, err))?; + + if entry + .file_name() + .as_bytes() + .iter() + .any(|&b| !b.is_ascii_digit()) + { + continue; + } + + let path = entry.path(); + + let md = path + .metadata() + .map_err(|err| LsLocksError::io1("reading file metadata", &path, err))?; + + if md.ino() == inode { + let path = entry + .path() + .read_link() + .map_err(|err| LsLocksError::io1("reading symbolic link", &path, err))?; + + return Ok((path, md.len())); + } + } + + Err(LsLocksError::io0( + "looking for inode open in process", + io::ErrorKind::NotFound, + )) +} + +pub(crate) struct BinFileLineIter { + path: PathBuf, + input: BufReader, + buffer: Vec, +} + +impl BinFileLineIter { + pub(crate) fn open(path: &Path) -> Result { + let input = File::open(path) + .map(BufReader::new) + .map_err(|err| LsLocksError::io1("opening file", path, err))?; + + Ok(Self { + path: path.into(), + input, + buffer: Vec::default(), + }) + } + + pub(crate) fn next_line(&mut self) -> Result, LsLocksError> { + self.buffer.clear(); + + let count = self + .input + .read_until(b'\n', &mut self.buffer) + .map_err(|err| LsLocksError::io1("reading file", &self.path, err))?; + if count == 0 { + return Ok(None); + } + + if self.buffer.last().copied() == Some(b'\n') { + self.buffer.pop(); + } + if self.buffer.last().copied() == Some(b'\r') { + self.buffer.pop(); + } + Ok(Some(&self.buffer)) + } +} + +pub(crate) struct LockInfo { + pub(crate) command_name: Option, + pub(crate) process_id: libc::pid_t, + pub(crate) path: Option, + pub(crate) kind: CString, + pub(crate) mode: CString, + pub(crate) range: Range, + pub(crate) inode: libc::ino_t, + pub(crate) device_id: libc::dev_t, + pub(crate) mandatory: bool, + pub(crate) blocked: bool, + pub(crate) size: Option, + pub(crate) file_descriptor: c_int, + pub(crate) id: i64, +} + +impl LockInfo { + pub(crate) fn parse( + no_inaccessible: bool, + fdinfo_path: &Path, + process_id: Option, + file_descriptor: c_int, + command_name: &CStr, + pid_locks: Option<&[Self]>, + line: &[u8], + ) -> Result, LsLocksError> { + let err_map = || { + let err = io::ErrorKind::InvalidData; + LsLocksError::io1("parsing lock information", fdinfo_path, err) + }; + + let mut elements = line + .split(|&b| b.is_ascii_whitespace()) + .filter(|&b| !b.is_empty()); + + let element = elements.next().ok_or_else(err_map)?; + + let id = if process_id.is_none() { + element + .strip_suffix(b":") + .ok_or_else(err_map) + .and_then(|b| std::str::from_utf8(b).map_err(|_| err_map()))? + .parse() + .map_err(|_| err_map())? + } else { + -1 + }; + + let mut blocked = false; + + let kind = loop { + let element = elements.next().ok_or_else(err_map)?; + if element != b"->" { + break CString::new(element).map_err(|_| err_map())?; + } + + blocked = true; + }; + + let mandatory = elements.next().ok_or_else(err_map)?.starts_with(b"M"); + + let element = elements.next().ok_or_else(err_map)?; + let mode = CString::new(element).map_err(|_| err_map())?; + + let mut unknown_command_name = false; + + let element = elements.next().ok_or_else(err_map)?; + + let (mut process_id, mut command_name) = if let Some(process_id) = process_id { + (process_id, Some(CString::from(command_name))) + } else { + let process_id: libc::pid_t = std::str::from_utf8(element) + .map_err(|_| err_map())? + .parse() + .map_err(|_| err_map())?; + + let command_name = if process_id > 0 { + if let Ok(cmd_line) = pid_command_name(process_id).map(Some) { + cmd_line + } else { + unknown_command_name = true; + None + } + } else { + None + }; + + (process_id, command_name) + }; + + let mut iter = elements.next().ok_or_else(err_map)?.split(|&b| b == b':'); + + let major = iter + .next() + .ok_or_else(err_map) + .and_then(|b| std::str::from_utf8(b).map_err(|_| err_map())) + .and_then(|s| c_uint::from_str_radix(s, 16).map_err(|_| err_map()))?; + let minor = iter + .next() + .ok_or_else(err_map) + .and_then(|b| std::str::from_utf8(b).map_err(|_| err_map())) + .and_then(|s| c_uint::from_str_radix(s, 16).map_err(|_| err_map()))?; + let inode = iter + .next() + .ok_or_else(err_map) + .and_then(|b| std::str::from_utf8(b).map_err(|_| err_map()))? + .parse() + .map_err(|_| err_map())?; + + let device_id = libc::makedev(major, minor); + + let element = elements.next().ok_or_else(err_map)?; + + let start = if element == b"EOF" { + 0 + } else { + std::str::from_utf8(element) + .map_err(|_| err_map())? + .parse() + .map_err(|_| err_map())? + }; + + let element = elements.next().ok_or_else(err_map)?; + + let end = if element == b"EOF" { + 0 + } else { + std::str::from_utf8(element) + .map_err(|_| err_map())? + .parse() + .map_err(|_| err_map())? + }; + + let range = start..end; + + if let Some(pid_locks) = pid_locks { + if command_name.is_none() && !blocked { + let lock_compare = |lock: &&LockInfo| { + lock.range == range + && lock.inode == inode + && lock.device_id == device_id + && lock.mandatory == mandatory + && lock.blocked == blocked + && lock.kind == kind + && lock.mode == mode + }; + + if let Some(found) = pid_locks.iter().find(lock_compare) { + process_id = found.process_id; + command_name = found.command_name.clone(); + } + } + } + + if command_name.is_none() { + command_name = if unknown_command_name { + Some(CString::from(c"(unknown)")) + } else { + Some(CString::from(c"(undefined)")) + }; + } + + let (mut path, size) = path_and_size_of_inode_opened_by_process(process_id, inode) + .ok() + .map_or((None, None), |(path, size)| (Some(path), Some(size))); + + if path.is_none() { + if no_inaccessible { + return Ok(None); + } + + path = fall_back_file_name(device_id).ok(); + } + + Ok(Some(Self { + command_name, + process_id, + path, + kind, + mode, + range, + inode, + device_id, + mandatory, + blocked, + size, + file_descriptor, + id, + })) + } +} + +fn fall_back_file_name(device_id: libc::dev_t) -> Result { + let table = unsafe { mnt_new_table_from_file(_PATH_PROC_MOUNTINFO_C.as_ptr()) }; + if table.is_null() { + return Err(LsLocksError::io1( + "mnt_new_table_from_file", + _PATH_PROC_MOUNTINFO, + io::ErrorKind::InvalidData, + )); + }; + + let fs = unsafe { mnt_table_find_devno(table, device_id, MNT_ITER_BACKWARD as c_int) }; + if fs.is_null() { + let err = io::ErrorKind::NotFound; + return Err(LsLocksError::io0("mnt_table_find_devno", err)); + }; + + let target = unsafe { mnt_fs_get_target(fs) }; + if target.is_null() { + let err = io::ErrorKind::NotFound; + return Err(LsLocksError::io0("mnt_fs_get_target", err)); + }; + + let target = unsafe { CStr::from_ptr(target) } + .to_str() + .map_err(|_| LsLocksError::io0("data is not UTF-8", io::ErrorKind::InvalidData))?; + + Ok(PathBuf::from(format!( + "{target}{}...", + if target.ends_with("/") { "" } else { "/" } + ))) +} diff --git a/tests/by-util/test_lslocks.rs b/tests/by-util/test_lslocks.rs index 3cd9dcd..10c9ce0 100644 --- a/tests/by-util/test_lslocks.rs +++ b/tests/by-util/test_lslocks.rs @@ -15,9 +15,8 @@ fn test_column_headers() { let header_line = stdout.lines().next().unwrap(); let cols: Vec<_> = header_line.split_whitespace().collect(); - assert_eq!(cols.len(), 7); assert_eq!( cols, - vec!["COMMAND", "PID", "TYPE", "MODE", "M", "START", "END"] + ["COMMAND", "PID", "TYPE", "SIZE", "MODE", "M", "START", "END", "PATH"] ); }