lfs-core-0.19.2/.cargo_vcs_info.json0000644000000001360000000000100126370ustar { "git": { "sha1": "e8294d722eb323ac4f4eca59caf31ac7196be0b7" }, "path_in_vcs": "" }lfs-core-0.19.2/.gitignore000064400000000000000000000000441046102023000134150ustar 00000000000000/target .bacon-locations Cargo.lock lfs-core-0.19.2/Cargo.lock0000644000000176660000000000100106320ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "io-kit-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" dependencies = [ "core-foundation-sys", "mach2", ] [[package]] name = "lazy-regex" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" dependencies = [ "proc-macro2", "quote", "regex", "syn 2.0.104", ] [[package]] name = "lfs-core" version = "0.19.2" dependencies = [ "core-foundation", "core-foundation-sys", "io-kit-sys", "lazy-regex", "libc", "snafu", "windows", ] [[package]] name = "libc" version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "mach2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "snafu" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" dependencies = [ "doc-comment", "snafu-derive", ] [[package]] name = "snafu-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" dependencies = [ "heck", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", "windows-core", "windows-future", "windows-numerics", ] [[package]] name = "windows-collections" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", ] [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-future" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core", "windows-link", "windows-threading", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn 2.0.104", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn 2.0.104", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core", "windows-link", ] [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-threading" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ "windows-link", ] lfs-core-0.19.2/Cargo.toml0000644000000032330000000000100106360ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "lfs-core" version = "0.19.2" authors = ["dystroy "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "give information on mounted disks" homepage = "https://dystroy.org/dysk" readme = "README.md" keywords = [ "linux", "macos", "filesystem", "fs", ] categories = ["filesystem"] license = "MIT" repository = "https://github.com/Canop/lfs-core" [lib] name = "lfs_core" path = "src/lib.rs" [dependencies.lazy-regex] version = "3.4" [dependencies.libc] version = "0.2" [dependencies.snafu] version = "0.7" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.core-foundation] version = "0.9" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.core-foundation-sys] version = "0.8" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.io-kit-sys] version = "0.4" [target."cfg(windows)".dependencies.windows] version = "0.62" features = [ "Win32_Storage_FileSystem", "Win32_System_SystemServices", "Win32_Security", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_WindowsProgramming", ] lfs-core-0.19.2/Cargo.toml.orig0000644000000015010000000000100115710ustar [package] name = "lfs-core" version = "0.19.2" authors = ["dystroy "] edition = "2021" keywords = ["linux", "macos", "filesystem", "fs"] license = "MIT" categories = ["filesystem"] description = "give information on mounted disks" repository = "https://github.com/Canop/lfs-core" homepage = "https://dystroy.org/dysk" readme = "README.md" [dependencies] lazy-regex = "3.4" libc = "0.2" snafu = "0.7" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] core-foundation = "0.9" core-foundation-sys = "0.8" io-kit-sys = "0.4" [target.'cfg(windows)'.dependencies] windows = { version = "0.62", features = [ "Win32_Storage_FileSystem", "Win32_System_SystemServices", "Win32_Security", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_WindowsProgramming", ] } lfs-core-0.19.2/Cargo.toml.orig000064400000000000000000000015011046102023000143130ustar 00000000000000[package] name = "lfs-core" version = "0.19.2" authors = ["dystroy "] edition = "2021" keywords = ["linux", "macos", "filesystem", "fs"] license = "MIT" categories = ["filesystem"] description = "give information on mounted disks" repository = "https://github.com/Canop/lfs-core" homepage = "https://dystroy.org/dysk" readme = "README.md" [dependencies] lazy-regex = "3.4" libc = "0.2" snafu = "0.7" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] core-foundation = "0.9" core-foundation-sys = "0.8" io-kit-sys = "0.4" [target.'cfg(windows)'.dependencies] windows = { version = "0.62", features = [ "Win32_Storage_FileSystem", "Win32_System_SystemServices", "Win32_Security", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_WindowsProgramming", ] } lfs-core-0.19.2/LICENSE000064400000000000000000000020571046102023000124400ustar 00000000000000MIT License Copyright (c) 2018 Denys Séguret Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lfs-core-0.19.2/README.md000064400000000000000000000012631046102023000127100ustar 00000000000000[![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] [s1]: https://img.shields.io/crates/v/lfs-core.svg [l1]: https://crates.io/crates/lfs-core [s2]: https://img.shields.io/badge/license-MIT-blue.svg [l2]: LICENSE [s3]: https://docs.rs/lfs-core/badge.svg [l3]: https://docs.rs/lfs-core/ [s4]: https://miaou.dystroy.org/static/shields/room.svg [l4]: https://miaou.dystroy.org/3 Give information on the mounted disks in linux, Mac, and Windows (experimental). **lfs-core** provides the data of [dysk](https://github.com/Canop/dysk) and of the `:fs` screen of [broot](https://dystroy.org/broot). You can also use the library in your own programs. lfs-core-0.19.2/bacon.toml000064400000000000000000000076421046102023000134170ustar 00000000000000# This is a configuration file for the bacon tool # # Complete help on configuration: https://dystroy.org/bacon/config/ # # You may check the current default at # https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml default_job = "check" env.CARGO_TERM_COLOR = "always" [jobs.check] command = ["cargo", "check"] need_stdout = false [jobs.check-all] command = ["cargo", "check", "--all-targets"] need_stdout = false # Run clippy on the default target [jobs.clippy] command = ["cargo", "clippy"] need_stdout = false # Run clippy on all targets # To disable some lints, you may change the job this way: # [jobs.clippy-all] # command = [ # "cargo", "clippy", # "--all-targets", # "--", # "-A", "clippy::bool_to_int_with_if", # "-A", "clippy::collapsible_if", # "-A", "clippy::derive_partial_eq_without_eq", # ] # need_stdout = false [jobs.clippy-all] command = ["cargo", "clippy", "--all-targets"] need_stdout = false [jobs.windows] command = [ "cross", "build", "--target", "x86_64-pc-windows-gnu", ] # Run clippy in pedantic mode # The 'dismiss' feature may come handy [jobs.pedantic] command = [ "cargo", "clippy", "--", "-W", "clippy::pedantic", "-A", "clippy::must_use_candidate", "-A", "clippy::missing_errors_doc", "-A", "clippy::struct_excessive_bools", "-A", "clippy::wildcard_imports", "-A", "clippy::return_self_not_must_use", ] need_stdout = false # This job lets you run # - all tests: bacon test # - a specific test: bacon test -- config::test_default_files # - the tests of a package: bacon test -- -- -p config [jobs.test] command = ["cargo", "test"] need_stdout = true [jobs.nextest] command = [ "cargo", "nextest", "run", "--hide-progress-bar", "--failure-output", "final" ] need_stdout = true analyzer = "nextest" [jobs.doc] command = ["cargo", "doc", "--no-deps"] need_stdout = false # If the doc compiles, then it opens in your browser and bacon switches # to the previous job [jobs.doc-open] command = ["cargo", "doc", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change # You can run your application and have the result displayed in bacon, # if it makes sense for this crate. [jobs.run] command = [ "cargo", "run", # put launch parameters for your program behind a `--` separator ] need_stdout = true allow_warnings = true background = true # Run your long-running application (eg server) and have the result displayed in bacon. # For programs that never stop (eg a server), `background` is set to false # to have the cargo run output immediately displayed instead of waiting for # program's end. # 'on_change_strategy' is set to `kill_then_restart` to have your program restart # on every change (an alternative would be to use the 'F5' key manually in bacon). # If you often use this job, it makes sense to override the 'r' key by adding # a binding `r = job:run-long` at the end of this file . # A custom kill command such as the one suggested below is frequently needed to kill # long running programs (uncomment it if you need it) [jobs.run-long] command = [ "cargo", "run", # put launch parameters for your program behind a `--` separator ] need_stdout = true allow_warnings = true background = false on_change_strategy = "kill_then_restart" # kill = ["pkill", "-TERM", "-P"] # This parameterized job runs the example of your choice, as soon # as the code compiles. # Call it as # bacon ex -- my-example [jobs.ex] command = ["cargo", "run", "--example"] need_stdout = true allow_warnings = true # You may define here keybindings that would be specific to # a project, for example a shortcut to launch a specific job. # Shortcuts to internal functions (scrolling, toggling, etc.) # should go in your personal global prefs.toml file instead. [keybindings] # alt-m = "job:my-job" c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target p = "job:pedantic" w = "job:windows" lfs-core-0.19.2/rustfmt.toml000064400000000000000000000001761046102023000140340ustar 00000000000000edition = "2021" style_edition = "2024" imports_granularity = "One" imports_layout = "Vertical" fn_params_layout = "Vertical" lfs-core-0.19.2/src/device_id/mod.rs000064400000000000000000000004511046102023000152560ustar 00000000000000use snafu::prelude::*; #[cfg(unix)] mod unix; #[cfg(windows)] mod windows; #[cfg(unix)] pub use unix::DeviceId; #[cfg(windows)] pub use windows::DeviceId; #[derive(Debug, Snafu)] #[snafu(display("Could not parse {string} as a device id"))] pub struct ParseDeviceIdError { string: String, } lfs-core-0.19.2/src/device_id/unix.rs000064400000000000000000000042701046102023000154650ustar 00000000000000use { super::{ ParseDeviceIdError, ParseDeviceIdSnafu, }, crate::Error, snafu::prelude::*, std::{ fmt, fs, os::unix::fs::MetadataExt, path::Path, str::FromStr, }, }; /// Id of a device, as can be found in `MetadataExt.dev()` #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct DeviceId { pub major: u32, pub minor: u32, } impl fmt::Display for DeviceId { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { write!(f, "{}:{}", self.major, self.minor) } } impl FromStr for DeviceId { type Err = ParseDeviceIdError; /// this code is based on `man 5 proc` and my stochastic interpretation fn from_str(string: &str) -> Result { (|| { let mut parts = string.split(':').fuse(); match (parts.next(), parts.next(), parts.next()) { (Some(major), Some(minor), None) => { let major = major.parse().ok()?; let minor = minor.parse().ok()?; Some(Self { major, minor }) } (Some(int), None, None) => { let int: u64 = int.parse().ok()?; Some(int.into()) } _ => None, } })() .with_context(|| ParseDeviceIdSnafu { string }) } } impl From for DeviceId { fn from(num: u64) -> Self { // need to use libc, bit format is platform-dependent let dev = num as libc::dev_t; Self { major: libc::major(dev) as u32, minor: libc::minor(dev) as u32, } } } impl DeviceId { pub fn new( major: u32, minor: u32, ) -> Self { Self { major, minor } } pub fn of_path(path: &Path) -> Result { let md = fs::metadata(path).map_err(|e| Error::CantReadFileMetadata { source: e, path: path.to_path_buf(), })?; let dev_num = md.dev(); Ok(Self::from(dev_num)) } } #[test] fn test_from_str() { assert_eq!(DeviceId::new(8, 16), DeviceId::from_str("8:16").unwrap()); } lfs-core-0.19.2/src/device_id/windows.rs000064400000000000000000000070541046102023000161770ustar 00000000000000use { super::{ ParseDeviceIdError, ParseDeviceIdSnafu, }, crate::WindowsApiSnafu, snafu::prelude::*, std::{ ffi::OsStr, fmt, os::windows::ffi::OsStrExt, path::Path, str::FromStr, }, windows::{ Win32::Storage::FileSystem::{ GetVolumeInformationW, GetVolumeNameForVolumeMountPointW, GetVolumePathNameW, }, core::PCWSTR, }, }; /// Id of a volume, can be found using GetVolumeInformationW #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct DeviceId { pub serial: u32, } impl fmt::Display for DeviceId { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { write!(f, "{:X}-{:X}", self.serial >> 16, self.serial & 0x0000_FFFF) } } impl FromStr for DeviceId { type Err = ParseDeviceIdError; fn from_str(string: &str) -> Result { if let Some((high, low)) = string.split_once('-') { if let (Ok(high), Ok(low)) = (u32::from_str_radix(high, 16), u32::from_str_radix(low, 16)) { let serial = (high << 16) | low; return Ok(Self { serial }); } } u32::from_str_radix(string, 16) .ok() .map(|serial| Self { serial }) .with_context(|| ParseDeviceIdSnafu { string }) } } impl From for DeviceId { fn from(num: u64) -> Self { Self { serial: num as u32 } } } impl From for DeviceId { fn from(num: u32) -> Self { Self { serial: num } } } impl DeviceId { pub fn new(serial: u32) -> Self { Self { serial } } /// Determine the DeviceId for a given file path pub fn of_path(path: &Path) -> Result { unsafe { let path_wide: Vec = OsStr::new(path) .encode_wide() .chain(std::iter::once(0)) // null terminator .collect(); // Step 1: Get the volume path from the file path let mut volume_path_buf = vec![0u16; 260]; // MAX_PATH GetVolumePathNameW(PCWSTR(path_wide.as_ptr()), &mut volume_path_buf).context( WindowsApiSnafu { api: "GetVolumePathNameW", }, )?; // Step 2: Get the volume GUID from the volume path let mut volume_guid_buf = vec![0u16; 260]; // MAX_PATH GetVolumeNameForVolumeMountPointW( PCWSTR(volume_path_buf.as_ptr()), &mut volume_guid_buf, ) .context(WindowsApiSnafu { api: "GetVolumeNameForVolumeMountPointW", })?; // Step 3: Get serial number from the GUID let mut serial: u32 = 0; GetVolumeInformationW( PCWSTR(volume_guid_buf.as_ptr()), None, Some(&mut serial), None, None, None, ) .context(WindowsApiSnafu { api: "GetVolumeInformationW", })?; Ok(Self { serial }) } } } #[test] fn test_from_str() { assert_eq!( DeviceId::new(0xABCD_1234), DeviceId::from_str("ABCD-1234").unwrap() ); assert_eq!( DeviceId::new(0xABCD_1234), DeviceId::from_str("ABCD1234").unwrap() ); } #[test] fn test_from_u64() { assert_eq!( DeviceId::new(0xFFFF_FFFF), DeviceId::from(0xFFFF_FFFF_FFFFu64) ); } lfs-core-0.19.2/src/disk.rs000064400000000000000000000030731046102023000135210ustar 00000000000000/// what we have most looking like a physical device #[derive(Debug, Clone)] pub struct Disk { /// a name, like "sda", "sdc", "nvme0n1", etc. #[cfg(not(windows))] pub name: String, /// true for HDD, false for SSD, None for unknown. /// This information isn't reliable for USB devices pub rotational: Option, /// whether the system thinks the media is removable. /// Seems reliable when not mapped pub removable: Option, /// whether the disk is read-only pub read_only: Option, /// whether it's a RAM disk pub ram: bool, /// disk image (Mac only right now) pub image: bool, /// whether it's on LVM pub lvm: bool, /// whether it's a crypted disk pub crypted: bool, /// whether it's a remote disk #[cfg(windows)] pub remote: bool, } impl Disk { /// a synthetic code trying to express the essence of the type of media, /// an empty str being returned when information couldn't be gathered. /// This code is for humans and may change in future minor versions. pub fn disk_type(&self) -> &'static str { if self.ram { "RAM" } else if self.image { "imag" } else if self.crypted { "crypt" } else if self.lvm { "LVM" } else { match (self.removable, self.rotational) { (Some(true), _) => "remov", (Some(false), Some(true)) => "HDD", (Some(false), Some(false)) => "SSD", _ => "", } } } } lfs-core-0.19.2/src/error.rs000064400000000000000000000025721046102023000137230ustar 00000000000000/// lfs error type #[derive(Debug, snafu::Snafu)] #[snafu(visibility(pub(crate)))] pub enum Error { #[snafu(display("Couldn't execute {exe}"))] CantExecute { source: std::io::Error, exe: String }, #[snafu(display("Could not read metadata of file {path:?}"))] CantReadFileMetadata { source: std::io::Error, path: std::path::PathBuf, }, #[snafu(display("Could not read file {path:?}"))] CantReadFile { source: std::io::Error, path: std::path::PathBuf, }, #[snafu(display("Could not read dir {path:?}"))] CantReadDir { source: std::io::Error, path: std::path::PathBuf, }, #[snafu(display("Could not parse mountinfo"))] #[cfg(target_os = "linux")] ParseMountInfo { source: crate::linux::ParseMountInfoError, }, #[snafu(display("Unexpected format"))] UnexpectedFormat, #[snafu(display("Error parsing device id"))] ParseDeviceId, #[snafu(display("Failed to call service {service:?}"))] ServiceCallFailed { service: &'static str }, #[snafu(display("Failed to read {key:?}"))] MissingValue { key: &'static str }, #[snafu(display("Device layer not found"))] DeviceLayerNotFound, #[cfg(windows)] #[snafu(display("Windows API call failed: {api}"))] WindowsApiError { source: windows::core::Error, api: String, }, } lfs-core-0.19.2/src/inodes.rs000064400000000000000000000020071046102023000140440ustar 00000000000000/// inode information /// /// This structure isn't built if data aren't consistent #[derive(Debug, Clone)] pub struct Inodes { /// number of inodes, always > 0 pub files: u64, /// number of free inodes pub ffree: u64, /// number of free inodes for underpriviledged users pub favail: u64, } impl Inodes { /// Create the structure if the given values are consistent, /// return None if they aren't. pub fn new( files: u64, ffree: u64, favail: u64, ) -> Option { if files > 0 && ffree <= files && favail <= files { Some(Self { files, ffree, favail, }) } else { None } } /// number of non available inodes, always > 0 pub fn used(&self) -> u64 { self.files - self.favail } /// share of non available inodes, always in [0, 1], never NaN pub fn use_share(&self) -> f64 { self.used() as f64 / self.files as f64 } } lfs-core-0.19.2/src/label.rs000064400000000000000000000032111046102023000136400ustar 00000000000000#[cfg(target_os = "linux")] use { super::*, snafu::prelude::*, std::fs, }; /// the labelling of a file-system, that is the pair (label, fs) #[derive(Debug, Clone)] pub struct Labelling { pub label: String, pub fs_name: String, } pub fn get_label( fs_name: &str, labellings: Option<&[Labelling]>, ) -> Option { labellings.as_ref().and_then(|labels| { labels .iter() .find(|label| label.fs_name == fs_name) .map(|label| label.label.clone()) }) } /// try to read all mappings defined in /dev/disk/by-, /// where by_kind is one of "label", "uuid", "partuuid", "diskseq", etc. /// /// An error can't be excluded as not all systems expose /// this information the way lfs-core reads it. #[cfg(target_os = "linux")] pub fn read_by(by_kind: &str) -> Result, Error> { let path = format!("/dev/disk/by-{by_kind}"); let entries = fs::read_dir(&path).context(CantReadDirSnafu { path })?; let labels = entries .filter_map(|entry| entry.ok()) .filter_map(|entry| { let md = entry.metadata().ok()?; let file_type = md.file_type(); if !file_type.is_symlink() { return None; } let label = sys::decode_string(entry.file_name().to_string_lossy()); let linked_path = fs::read_link(entry.path()) .map(|path| path.to_string_lossy().to_string()) .ok()?; let fs_name = format!("/dev/{}", linked_path.strip_prefix("../../")?,); Some(Labelling { label, fs_name }) }) .collect(); Ok(labels) } lfs-core-0.19.2/src/lib.rs000064400000000000000000000021071046102023000133320ustar 00000000000000/*! Use `lfs_core::read_mounts` to get information on all mounted volumes. ``` // get all mount points let options = lfs_core::ReadOptions::default(); let mut mounts = lfs_core::read_mounts(&options).unwrap(); // only keep the one with size stats mounts.retain(|m| m.stats.is_ok()); // print them for mount in mounts { dbg!(mount); } ``` The [dysk](https://github.com/Canop/dysk) application is a viewer for lfs-core and shows you the information you're expected to find in mounts. */ mod device_id; mod disk; mod error; mod inodes; mod label; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] mod macos; mod mount; mod mountinfo; mod read_options; mod stats; mod sys; #[cfg(windows)] mod windows; pub use { device_id::*, disk::*, error::*, inodes::*, label::*, mount::*, mountinfo::*, read_options::*, stats::*, }; #[cfg(target_os = "linux")] pub use linux::read_mounts; #[cfg(target_os = "macos")] pub use macos::read_mounts; #[cfg(windows)] pub use windows::read_mounts; #[cfg(windows)] pub use windows::volume_serial_for_path; lfs-core-0.19.2/src/linux/block_device.rs000064400000000000000000000060431046102023000163370ustar 00000000000000use { crate::*, snafu::prelude::*, std::{ fs, path::{ Path, PathBuf, }, str::FromStr, }, }; /// the list of all found block devices #[derive(Debug, Clone)] pub struct BlockDeviceList { list: Vec, } /// a "block device", that is a device listed in /// the /sys/block tree with a device id #[derive(Debug, Clone)] pub struct BlockDevice { pub name: String, /// a name for a /dev/mapper/ device pub dm_name: Option, pub id: DeviceId, pub parent: Option, } impl BlockDeviceList { pub fn read() -> Result { let mut list = Vec::new(); let root = PathBuf::from("/sys/block"); append_child_block_devices(None, &root, &mut list, 0)?; Ok(Self { list }) } pub fn find_by_id( &self, id: DeviceId, ) -> Option<&BlockDevice> { self.list.iter().find(|bd| bd.id == id) } pub fn find_by_dm_name( &self, dm_name: &str, ) -> Option<&BlockDevice> { self.list .iter() .find(|bd| bd.dm_name.as_ref().is_some_and(|s| s == dm_name)) } pub fn find_by_name( &self, name: &str, ) -> Option<&BlockDevice> { self.list.iter().find(|bd| bd.name == name) } pub fn find_top( &self, id: DeviceId, dm_name: Option<&str>, name: Option<&str>, ) -> Option<&BlockDevice> { self.find_by_id(id) .or_else(|| dm_name.and_then(|dm_name| self.find_by_dm_name(dm_name))) .or_else(|| name.and_then(|name| self.find_by_name(name))) .and_then(|bd| match bd.parent { Some(parent_id) => self.find_top(parent_id, None, None), None => Some(bd), }) } } fn append_child_block_devices( parent: Option, parent_path: &Path, list: &mut Vec, depth: usize, ) -> Result<(), Error> { let children = fs::read_dir(parent_path).with_context(|_| CantReadDirSnafu { path: parent_path.to_path_buf(), })?; for e in children.flatten() { let device_id = fs::read_to_string(e.path().join("dev")) .ok() .and_then(|s| DeviceId::from_str(s.trim()).ok()); if let Some(id) = device_id { if list.iter().any(|bd| bd.id == id) { // already present, probably because of a cycling link continue; } let name = e.file_name().to_string_lossy().to_string(); let dm_name = sys::read_file(format!("/sys/block/{name}/dm/name")) .ok() .map(|s| s.trim().to_string()); list.push(BlockDevice { name, dm_name, id, parent, }); if depth > 15 { // there's probably a link cycle continue; } append_child_block_devices(Some(id), &e.path(), list, depth + 1)?; } } Ok(()) } lfs-core-0.19.2/src/linux/mod.rs000064400000000000000000000111211046102023000144760ustar 00000000000000mod block_device; mod read_mountinfos; use { crate::*, block_device::*, lazy_regex::*, std::{ ffi::CString, mem, os::unix::ffi::OsStrExt, path::Path, sync::mpsc, thread, time::Duration, }, }; pub use read_mountinfos::ParseMountInfoError; pub fn new_disk(name: String) -> Disk { let rotational = sys::read_file_as_bool(format!("/sys/block/{name}/queue/rotational")); let removable = sys::read_file_as_bool(format!("/sys/block/{name}/removable")); let ram = regex_is_match!(r#"^zram\d*$"#, &name); let dm_uuid = sys::read_file(format!("/sys/block/{name}/dm/uuid")).ok(); let crypted = dm_uuid .as_ref() .is_some_and(|uuid| uuid.starts_with("CRYPT-")); let lvm = dm_uuid.is_some_and(|uuid| uuid.starts_with("LVM-")); Disk { name, rotational, removable, image: false, read_only: None, ram, lvm, crypted, } } /// Read all the mount points and load basic information on them pub fn read_mounts(options: &ReadOptions) -> Result, Error> { let by_label = read_by("label").ok(); let by_uuid = read_by("uuid").ok(); let by_partuuid = read_by("partuuid").ok(); // we'll find the disk for a filesystem by taking the longest // disk whose name starts the one of our partition // hence the sorting. let bd_list = BlockDeviceList::read()?; read_mountinfos::read_all_mountinfos()? .drain(..) .map(|info| { let top_bd = bd_list.find_top(info.dev, info.dm_name(), info.fs_name()); let fs_label = get_label(&info.fs, by_label.as_deref()); let uuid = get_label(&info.fs, by_uuid.as_deref()); let part_uuid = get_label(&info.fs, by_partuuid.as_deref()); let disk = top_bd.map(|bd| new_disk(bd.name.clone())); let stats = if info.is_remote() && !options.remote_stats { Err(StatsError::Excluded) } else if let Some(timeout) = options.stats_timeout { read_stats_with_timeout(&info.mount_point, timeout) } else { read_stats(&info.mount_point) }; Ok(Mount { info, fs_label, disk, stats, uuid, part_uuid, }) }) .collect() } pub fn read_stats_with_timeout( mount_point: &Path, timeout: Duration, ) -> Result { let mount_point = mount_point.to_path_buf(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { let stats = read_stats(&mount_point); let _ = tx.send(stats); }); rx.recv_timeout(timeout).map_err(|_| StatsError::Timeout)? } pub fn read_stats(mount_point: &Path) -> Result { let c_mount_point = CString::new(mount_point.as_os_str().as_bytes()).unwrap(); unsafe { let mut statvfs = mem::MaybeUninit::::uninit(); let code = libc::statvfs(c_mount_point.as_ptr(), statvfs.as_mut_ptr()); match code { 0 => { let statvfs = statvfs.assume_init(); // blocks info let bsize = statvfs.f_bsize; let blocks = statvfs.f_blocks; let bfree = statvfs.f_bfree; let bavail = statvfs.f_bavail; if bsize == 0 || blocks == 0 || bfree > blocks || bavail > blocks { // unconsistent or void data return Err(StatsError::Unconsistent); } // statvfs doesn't provide bused let bused = blocks - bavail; // inodes info, will be checked in Inodes::new let files = statvfs.f_files; let ffree = statvfs.f_ffree; let favail = statvfs.f_favail; #[allow(clippy::useless_conversion)] let inodes = Inodes::new(files.into(), ffree.into(), favail.into()); #[allow(clippy::useless_conversion)] Ok(Stats { bsize: bsize.into(), blocks: blocks.into(), bused: bused.into(), bfree: bfree.into(), bavail: bavail.into(), inodes: inodes.into(), }) } _ => { // the filesystem wasn't found, it's a strange one, for example a // docker one, or a disconnected remote one Err(StatsError::Unreachable) } } } } lfs-core-0.19.2/src/linux/read_mountinfos.rs000064400000000000000000000132521046102023000171220ustar 00000000000000use { crate::*, lazy_regex::*, snafu::prelude::*, std::path::PathBuf, }; #[derive(Debug, Snafu)] #[snafu(display("Could not parse {line} as mount info"))] pub struct ParseMountInfoError { line: String, } #[cfg(target_os = "linux")] impl std::str::FromStr for MountInfo { type Err = ParseMountInfoError; fn from_str(line: &str) -> Result { (|| { // this parsing is based on `man 5 proc` // Structure is also visible at // https://man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html let mut tokens = line.split_whitespace(); let id = tokens.next()?.parse().ok()?; let parent = tokens.next()?.parse().ok()?; // while linux mountinfo need an id and a parent id, they're optional in // the more global model let id = Some(id); let parent = Some(parent); let dev = tokens.next()?.parse().ok()?; let root = str_to_pathbuf(tokens.next()?); let mount_point = str_to_pathbuf(tokens.next()?); let direct_options = regex_captures_iter!("(?:^|,)([^=,]+)(?:=([^=,]*))?", tokens.next()?,); let mut options: Vec = direct_options .map(|c| { let name = c.get(1).unwrap().as_str().to_string(); let value = c.get(2).map(|v| v.as_str().to_string()); MountOption { name, value } }) .collect(); // skip optional fields in the form name:value where // name can be "shared", "master", "propagate_for", or "unbindable" loop { let token = tokens.next()?; if token == "-" { break; } } let fs_type = tokens.next()?.to_string(); let fs = tokens.next()?.to_string(); if let Some(super_options) = tokens.next() { for c in regex_captures_iter!("(?:^|,)([^=,]+)(?:=([^=,]*))?", super_options) { let name = c.get(1).unwrap().as_str().to_string(); if name == "rw" { continue; // rw at super level is not relevant } if options.iter().any(|o| o.name == name) { continue; } let value = c.get(2).map(|v| v.as_str().to_string()); options.push(MountOption { name, value }); } } Some(Self { id, parent, dev, root, mount_point, options, fs, fs_type, bound: false, // determined by post-treatment }) })() .with_context(|| ParseMountInfoSnafu { line }) } } /// convert a string to a pathbuf, converting ascii-octal encoded /// chars. /// This is necessary because some chars are encoded. For example /// the `/media/dys/USB DISK` is present as `/media/dys/USB\040DISK` #[cfg(target_os = "linux")] fn str_to_pathbuf(s: &str) -> PathBuf { PathBuf::from(sys::decode_string(s)) } /// read all the mount points #[cfg(target_os = "linux")] pub fn read_all_mountinfos() -> Result, Error> { let mut mounts: Vec = Vec::new(); let path = "/proc/self/mountinfo"; let file_content = sys::read_file(path).context(CantReadDirSnafu { path })?; for line in file_content.trim().split('\n') { let mut mount: MountInfo = line .parse() .map_err(|source| Error::ParseMountInfo { source })?; mount.bound = mounts.iter().any(|m| m.dev == mount.dev); mounts.push(mount); } Ok(mounts) } #[cfg(target_os = "linux")] #[allow(clippy::bool_assert_comparison)] #[test] fn test_from_str() { use std::str::FromStr; let mi = MountInfo::from_str( "47 21 0:41 / /dev/hugepages rw,relatime shared:27 - hugetlbfs hugetlbfs rw,pagesize=2M", ) .unwrap(); assert_eq!(mi.id, Some(47)); assert_eq!(mi.dev, DeviceId::new(0, 41)); assert_eq!(mi.root, PathBuf::from("/")); assert_eq!(mi.mount_point, PathBuf::from("/dev/hugepages")); assert_eq!(mi.options_string(), "rw,relatime,pagesize=2M".to_string()); let mi = MountInfo::from_str( "106 26 8:17 / /home/dys/dev rw,noatime,compress=zstd:3 shared:57 - btrfs /dev/sdb1 rw,attr2,inode64,noquota" ).unwrap(); assert_eq!(mi.id, Some(106)); assert_eq!(mi.dev, DeviceId::new(8, 17)); assert_eq!(&mi.fs, "/dev/sdb1"); assert_eq!(&mi.fs_type, "btrfs"); let mut options = mi.options.clone().into_iter(); assert_eq!(options.next(), Some(MountOption::new("rw", None)),); assert_eq!(options.next(), Some(MountOption::new("noatime", None))); assert_eq!( options.next(), Some(MountOption::new("compress", Some("zstd:3"))) ); assert_eq!(mi.has_option("noatime"), true); assert_eq!(mi.has_option("relatime"), false); assert_eq!(mi.option_value("thing"), None); assert_eq!(mi.option_value("compress"), Some("zstd:3")); assert_eq!( mi.options_string(), "rw,noatime,compress=zstd:3,attr2,inode64,noquota".to_string() ); let mi = MountInfo::from_str( "73 2 0:33 /root / rw,relatime shared:1 - btrfs /dev/vda3 rw,seclabel,compress=zstd:1,ssd,space_cache=v2,subvolid=256,subvol=/root" ).unwrap(); assert_eq!(mi.option_value("compress"), Some("zstd:1")); assert_eq!( mi.options_string(), "rw,relatime,seclabel,compress=zstd:1,ssd,space_cache=v2,subvolid=256,subvol=/root" .to_string() ); } lfs-core-0.19.2/src/macos/diskutil/diskutil_exec.rs000064400000000000000000000016031046102023000203520ustar 00000000000000use { crate::error::*, lazy_regex::*, snafu::prelude::*, std::process, }; pub fn du_lines(args: &[&str]) -> Result, Error> { let exe = "diskutil"; let output = process::Command::new(exe) .args(args) .output() .with_context(|_| CantExecuteSnafu { exe })?; let output = str::from_utf8(&output.stdout).map_err(|_| Error::UnexpectedFormat)?; let lines = output.lines().map(|s| s.to_string()).collect(); Ok(lines) } /// return all container/volume identifiers, which are strings like "disk6" or "disk3s3s3" #[allow(dead_code)] pub fn du_identifiers() -> Result, Error> { let mut ids = Vec::new(); for line in du_lines(&["list"])? { let Some((_, id)) = regex_captures!(r"^\s+\d+:.+\s\wB\s+(disk\w+)$", &line) else { continue; }; ids.push(id.to_string()); } Ok(ids) } lfs-core-0.19.2/src/macos/diskutil/diskutil_read.rs000064400000000000000000000106261046102023000203460ustar 00000000000000use { super::{ DuDevice, diskutil_exec::*, }, crate::*, lazy_regex::*, }; pub fn mounted_du_devices() -> Result, Error> { let lines = du_lines(&["info", "-all"])?; Ok(lines_to_devices(&lines)) } fn lines_to_devices(lines: &[String]) -> Vec { let mut devs = Vec::new(); let mut start = 0; for (i, line) in lines.iter().enumerate() { if regex_is_match!(r"\s*\*{8,}\s*$", line) { if i > start + 3 { let dev_lines = &lines[start..i]; if let Some(dev) = lines_to_device(dev_lines) { devs.push(dev); } else { eprintln!("Device not understood:\n{}", dev_lines.join("\n"),); } } start = i + 1; } } devs } fn lines_to_device(lines: &[String]) -> Option { let mut id = None; let mut node = None; let mut file_system = None; let mut mount_point = None; let mut part_of_whole = None; let mut encrypted = None; let mut read_only = None; let mut removable = None; let mut protocol = None; let mut solid_state = None; let mut volume_total_space = None; let mut volume_free_space = None; let mut volume_used_space = None; let mut container_total_space = None; let mut container_free_space = None; let mut allocation_block_size = None; let mut uuid = None; let mut part_uuid = None; for line in lines { let Some((_, key, value)) = regex_captures!(r"^\s+([^\:]+):\s+(.+)$", &line) else { continue; }; match key { "Device Identifier" => { id = Some(value.to_string()); } "Device Node" => { node = Some(value.to_string()); } "File System" | "File System Personality" => { if value != "None" { file_system = Some(value.to_string()); } } "Mount Point" => { mount_point = Some(value.to_string()); } "Part of Whole" => { part_of_whole = Some(value.to_string()); } "Protocol" => { protocol = Some(value.to_string()); } "Encrypted" => { encrypted = extract_bool(value); } "Media Read-Only" => { read_only = extract_bool(value); } "Removable Media" => match value { "Removable" => { removable = Some(true); } "Fixed" => { removable = Some(false); } _ => {} }, "Solid State" => { solid_state = extract_bool(value); } "Volume Total Space" => { volume_total_space = extract_bytes(value); } "Volume Free Space" => { volume_free_space = extract_bytes(value); } "Volume Used Space" => { volume_used_space = extract_bytes(value); } "Container Total Space" => { container_total_space = extract_bytes(value); } "Container Free Space" => { container_free_space = extract_bytes(value); } "Allocation Block Size" => { allocation_block_size = extract_bytes(value); } "Volume UUID" => { uuid = Some(value.to_string()); } "Disk / Partition UUID" => { part_uuid = Some(value.to_string()); } _ => {} } } Some(DuDevice { id: id?, node: node?, file_system, mount_point, part_of_whole, removable, protocol, solid_state, read_only, encrypted, volume_total_space, volume_free_space, volume_used_space, container_total_space, container_free_space, allocation_block_size, uuid, part_uuid, }) } fn extract_bytes(s: &str) -> Option { let (_, num) = regex_captures!(r"(\d+)\sBytes", s)?; num.parse().ok() } fn extract_bool(value: &str) -> Option { regex_switch!(value, r"^Yes\b" => true, r"^No\b" => false, ) } lfs-core-0.19.2/src/macos/diskutil/mod.rs000064400000000000000000000072741046102023000163070ustar 00000000000000mod diskutil_exec; mod diskutil_read; use { crate::*, snafu::prelude::*, std::{ fs, os::unix::fs::MetadataExt, }, }; #[derive(Debug)] struct DuDevice { id: String, // ex: "disk3s3s1" node: String, // ex: "/dev/disk3s3s1" file_system: Option, // ex: "APFS" mount_point: Option, // ex: "/" part_of_whole: Option, // ex: "disk3" protocol: Option, removable: Option, read_only: Option, solid_state: Option, encrypted: Option, volume_total_space: Option, volume_used_space: Option, volume_free_space: Option, container_total_space: Option, container_free_space: Option, allocation_block_size: Option, uuid: Option, part_uuid: Option, } impl DuDevice { pub fn stats(&self) -> Option { let bsize = self.allocation_block_size?; let total = self.volume_total_space.or(self.container_total_space)?; let blocks = total / bsize; let bused = self.volume_used_space? / bsize; let free = self.volume_free_space.or(self.container_free_space)?; let bfree = free / bsize; let bavail = bfree; Some(Stats { bsize, blocks, bused, bfree, bavail, inodes: None, // TODO }) } } /// Get the device id from the BSD device node /// /// eg /dev/disk3s4 -> 1:13 fn query_device_id(device_node: &str) -> Result { let stat = fs::metadata(device_node).with_context(|_| CantReadFileSnafu { path: device_node })?; let rdev = stat.rdev(); let device_id = DeviceId::from(rdev); Ok(device_id) } /// Read all the mount points and load basic information on them pub fn read_mounts(_options: &ReadOptions) -> Result, Error> { let devs = diskutil_read::mounted_du_devices()?; let mut mounts = Vec::new(); for dev in devs { let stats = dev.stats().ok_or(StatsError::Unreachable); let DuDevice { id, node, file_system, part_of_whole, read_only, encrypted, protocol, mount_point, removable, solid_state, uuid, part_uuid, .. } = dev; let Some(mount_point) = mount_point else { continue; }; let Some(file_system) = file_system else { continue; }; let image = matches!(protocol.as_deref(), Some("Disk Image")); let disk = Disk { name: part_of_whole.as_ref().unwrap_or(&id).to_string(), rotational: solid_state.map(|s| !s), removable, image, read_only, ram: false, lvm: false, crypted: encrypted.unwrap_or(false), }; let dev = query_device_id(&node)?; let mut info = MountInfo { id: None, parent: None, dev, root: mount_point.clone().into(), // unsure mount_point: mount_point.into(), options: Default::default(), fs: node, fs_type: file_system, bound: false, // FIXME unsure (as for root) }; if let Some(shortened) = info.fs_type.strip_prefix("MS-DOS ") { info.fs_type = shortened.to_string(); } let mount = Mount { info, fs_label: None, // TODO disk: Some(disk), stats, uuid, part_uuid, }; mounts.push(mount); } Ok(mounts) } lfs-core-0.19.2/src/macos/iokit/dev_mount_info.rs000064400000000000000000000136121046102023000200230ustar 00000000000000use { crate::*, libc::{ MNT_NOWAIT, getfsstat, statfs, }, }; /// Basically data coming from getfsstat (BSD style) #[derive(Debug)] pub struct DevMountInfo { pub device: String, pub dev: DeviceId, pub mount_point: String, pub fs_type: String, pub stats: Stats, pub options: Vec, } impl DevMountInfo { pub fn to_mount_info(&self) -> MountInfo { MountInfo { id: None, parent: None, dev: self.dev, root: self.mount_point.clone().into(), mount_point: self.mount_point.clone().into(), options: self.options.clone(), fs: self.device.clone(), fs_type: self.fs_type.clone(), bound: false, } } pub fn get_all() -> Vec { unsafe { // First call to get the number of filesystems let count = getfsstat(std::ptr::null_mut(), 0, MNT_NOWAIT); if count <= 0 { return Vec::new(); } // Allocate buffer let mut buf: Vec = Vec::with_capacity(count as usize); let buf_size = (count as usize) * std::mem::size_of::(); // Second call to get the data let actual_count = getfsstat(buf.as_mut_ptr(), buf_size as i32, MNT_NOWAIT); if actual_count <= 0 { return Vec::new(); } buf.set_len(actual_count as usize); buf.into_iter() .filter_map(|stat| { let device = std::ffi::CStr::from_ptr(stat.f_mntfromname.as_ptr()) .to_str() .ok()?; let fsid: u64 = std::mem::transmute_copy(&stat.f_fsid); let dev = fsid.into(); let mount_point = std::ffi::CStr::from_ptr(stat.f_mntonname.as_ptr()) .to_str() .ok()?; let fs_type = std::ffi::CStr::from_ptr(stat.f_fstypename.as_ptr()) .to_str() .ok()?; let stats = Stats { bsize: stat.f_bsize as u64, blocks: stat.f_blocks, bfree: stat.f_bfree, bavail: stat.f_bavail, bused: stat.f_blocks - stat.f_bavail, inodes: None, }; let fs_type = match fs_type { "apfs" => "APFS", "exfat" => "ExFAT", "ftp" => "FTP", "hfs" => "HFS+", "msdos" if stats.bsize * stats.blocks > 2_147_484_648 => "FAT32", "msdos" => "FAT", // will be detemined using device.content "nfs" => "NFS", "ntfs" => "NTFS", "udf" => "UDF", "ufs" => "UFS", "xfs" => "XHS", "zfs" => "ZFS", v => v, // other ones unchanged }; // we'll try to build a "mount options" array consistent with the semantics of linux // Constants are defined in https://github.com/apple/darwin-xnu/blob/main/bsd/sys/mount.h // I'm not sure how stable those flag values are let flags: u32 = stat.f_flags; let mut options = Vec::new(); if flags & 1 == 0 { // MNT_READ_ONLY = 1 options.push(MountOption::new("rw", None)); } if flags & 2 != 0 { // MNT_SYNCHRONOUS = 2 options.push(MountOption::new("synchronous", None)); } if flags & 4 != 0 { // MNT_NOEXEC = 4 options.push(MountOption::new("noexec", None)); } if flags & 8 != 0 { // MNT_NOSUID = 8 options.push(MountOption::new("nosuid", None)); } if flags & 16 != 0 { // MNT_NODEV = 16 options.push(MountOption::new("nodev", None)); } if flags & 32 != 0 { // MNT_UNION = 32 options.push(MountOption::new("union", None)); } if flags & 64 != 0 { // MNT_ASYNC = 64 options.push(MountOption::new("async", None)); } if flags & 128 != 0 { // MNT_CPROTECT = 128 options.push(MountOption::new("cprotect", None)); } if flags & 512 != 0 { // MNT_REMOVABLE = 512 options.push(MountOption::new("removable", None)); } // Following ones don't seem correct // if flags & 0x00100000 != 0 { // MNT_DONTBROWSE = 0x00100000 // options.push(MountOption::new("dontbrowse", None)); // } // if flags & 0x10000000 != 0 { // MNT_NOATIME = 0x10000000 // options.push(MountOption::new("noatime", None)); // } Some(DevMountInfo { device: device.to_string(), dev, mount_point: mount_point.to_string(), fs_type: fs_type.to_string(), stats, options, }) }) .collect() } } } lfs-core-0.19.2/src/macos/iokit/mod.rs000064400000000000000000000122621046102023000155670ustar 00000000000000mod dev_mount_info; mod properties; use { crate::*, dev_mount_info::DevMountInfo, io_kit_sys::{ IOIteratorNext, IOObjectRelease, IORegistryEntryGetParentEntry, IOServiceGetMatchingServices, IOServiceMatching, kIOMasterPortDefault, keys::kIOServicePlane, types::{ io_iterator_t, io_object_t, }, }, lazy_regex::*, libc::KERN_SUCCESS, properties::Properties, std::os::raw::c_char, }; /// Data coming from IOKit and related to a mounted device #[derive(Debug)] pub struct Device { id: String, // eg "disk3s3s1" node: String, // eg "/dev/disk3s3s1" removable: Option, read_only: Option, crypted: Option, rotational: Option, uuid: Option, part_uuid: Option, content: Option, // eg "Windows_FAT_16" } /// Read all the mount points and load information on them pub fn read_mounts(_options: &ReadOptions) -> Result, Error> { let devs = mounted_devices()?; let dmis = DevMountInfo::get_all(); let mut mounts = Vec::new(); for dmi in dmis { let mut info = dmi.to_mount_info(); let dev = devs.iter().find(|dev| dev.node == dmi.device); if info.fs_type == "FAT" { if let Some(dev) = dev.as_ref() { if let Some(content) = dev.content.as_ref() { if let Some((_, v)) = regex_captures!(r"^\w+_FAT_(\d\d)$", content) { info.fs_type = format!("FAT{v}"); } } } } let disk = dev.as_ref().map(|dev| Disk { name: dev.id.clone(), rotational: dev.rotational, removable: dev.removable, read_only: dev.read_only, ram: false, image: false, lvm: false, crypted: dev.crypted.unwrap_or_default(), }); let mount = Mount { info, fs_label: None, // TODO disk, stats: Ok(dmi.stats.clone()), uuid: dev.as_ref().and_then(|d| d.uuid.clone()), part_uuid: dev.as_ref().and_then(|d| d.part_uuid.clone()), }; mounts.push(mount); } Ok(mounts) } pub fn mounted_devices() -> Result, Error> { let mut devs = Vec::new(); unsafe { let dict = IOServiceMatching(c"IOMedia".as_ptr() as *const c_char); if dict.is_null() { return Err(Error::ServiceCallFailed { service: "IOServiceMatching/IOMedia", }); } let mut iterator: io_iterator_t = 0; let result = IOServiceGetMatchingServices(kIOMasterPortDefault, dict, &mut iterator); if result != KERN_SUCCESS { return Err(Error::ServiceCallFailed { service: "IOServiceGetMatchingServices", }); } let mut media_service: io_object_t; while { media_service = IOIteratorNext(iterator); media_service != 0 } { let dev = service_to_device(media_service)?; devs.push(dev); IOObjectRelease(media_service); } IOObjectRelease(iterator); } //dbg!(dmis); Ok(devs) } unsafe fn service_to_device( media_service: io_object_t, // service from the IOMedia layer ) -> Result { let mut current_service = media_service; let mut parent: io_object_t = 0; loop { let result = IORegistryEntryGetParentEntry(current_service, kIOServicePlane, &mut parent); if result != KERN_SUCCESS { break; } let props = Properties::new(parent)?; if props.has("Device Characteristics") || props.has("Solid State") { // this is the "physical" layer let media_props = Properties::new(media_service)?; let device = props_to_device(media_props, props)?; IOObjectRelease(current_service); return Ok(device); } if current_service != media_service { IOObjectRelease(current_service); } current_service = parent; } Err(Error::DeviceLayerNotFound) } fn props_to_device( media_props: Properties, bs_props: Properties, // block storage layer ) -> Result { let id = media_props.get_mandatory_string("BSD Name")?; let node = format!("/dev/{id}"); let removable = media_props.get_bool("Removable"); let crypted = media_props.get_bool("CoreStorage Encrypted"); // TODO check this let read_only = media_props.get_bool("Writable").map(|b| !b); let medium_type = bs_props.get_sub_string("Device Characteristics", "Medium Type"); let rotational = medium_type.map(|v| !v.contains("Solid")); let uuid = media_props.get_string("UUID"); let part_uuid = None; // TODO let content = media_props.get_string("Content"); Ok(Device { id, node, removable, crypted, rotational, read_only, uuid, part_uuid, content, }) } #[test] fn test_smb() { let mountinfos = get_all_dev_mount_infos(); println!("MountInfos: {:#?}", mountinfos); todo!(); } lfs-core-0.19.2/src/macos/iokit/properties.rs000064400000000000000000000076011046102023000172050ustar 00000000000000use { crate::Error, core_foundation::{ base::*, boolean::{ CFBoolean, CFBooleanGetTypeID, }, dictionary::{ CFDictionary, CFDictionaryGetValue, CFDictionaryRef, CFMutableDictionaryRef, }, number::{ CFNumber, CFNumberGetTypeID, }, string::{ CFString, CFStringGetTypeID, CFStringRef, }, }, io_kit_sys::{ IORegistryEntryCreateCFProperties, types::*, }, libc::KERN_SUCCESS, std::{ ffi::c_void, mem, }, }; pub struct Properties { dict: CFDictionary, } impl Properties { pub unsafe fn new(service: io_object_t) -> Result { let mut dict = mem::MaybeUninit::::uninit(); let result = IORegistryEntryCreateCFProperties(service, dict.as_mut_ptr(), kCFAllocatorDefault, 0); if result != KERN_SUCCESS { return Err(Error::ServiceCallFailed { service: "IORegistryEntryCreateCFProperties", }); } let dict = CFDictionary::wrap_under_create_rule(dict.assume_init()); //dict.show(); Ok(Self { dict }) } pub fn has( &self, key: &'static str, ) -> bool { let key = CFString::from_static_string(key); self.dict.contains_key(&key) } pub fn get_mandatory_string( &self, key: &'static str, ) -> Result { self.get_string(key).ok_or(Error::MissingValue { key }) } #[allow(dead_code)] pub fn get_mandatory_u64( &self, key: &'static str, ) -> Result { self.get_u64(key).ok_or(Error::MissingValue { key }) } pub fn get_sub_string( &self, dict_key: &'static str, prop_key: &'static str, ) -> Option { let dict_key = CFString::from_static_string(dict_key); let dict = self.dict.find(&dict_key)?; let dict = dict.as_CFTypeRef() as CFDictionaryRef; let prop_key = CFString::from_static_string(prop_key); let value = unsafe { CFDictionaryGetValue(dict, prop_key.as_concrete_TypeRef() as *const c_void) }; if value.is_null() { return None; } let value = unsafe { CFString::wrap_under_get_rule(value as CFStringRef) }; Some(value.to_string()) } pub fn get_string( &self, key: &'static str, ) -> Option { let key = CFString::from_static_string(key); self.dict .find(&key) .and_then(|value_ref| { unsafe { debug_assert!(value_ref.type_of() == CFStringGetTypeID()); } value_ref.downcast::() }) .map(|cf_string| cf_string.to_string()) } pub fn get_u64( &self, key: &'static str, ) -> Option { let key = CFString::from_static_string(key); self.dict .find(&key) .and_then(|value_ref| { unsafe { debug_assert!(value_ref.type_of() == CFNumberGetTypeID()); } value_ref.downcast::() }) .and_then(|cf_value| cf_value.to_i64()) .and_then(|v| v.try_into().ok()) } pub fn get_bool( &self, key: &'static str, ) -> Option { let key = CFString::from_static_string(key); self.dict .find(&key) .and_then(|value_ref| { unsafe { debug_assert!(value_ref.type_of() == CFBooleanGetTypeID()); } value_ref.downcast::() }) .map(|cf_value| cf_value.into()) } } lfs-core-0.19.2/src/macos/mod.rs000064400000000000000000000010651046102023000144470ustar 00000000000000mod diskutil; mod iokit; use crate::*; /// Read all the mount points and load information on them pub fn read_mounts(options: &ReadOptions) -> Result, Error> { match options.strategy { Some(Strategy::Iokit) => { eprintln!("Calling IOKit to read device information"); iokit::read_mounts(options) } Some(Strategy::Diskutil) => { eprintln!("Calling diskutil to read device information"); diskutil::read_mounts(options) } _ => iokit::read_mounts(options), } } lfs-core-0.19.2/src/mount.rs000064400000000000000000000032261046102023000137310ustar 00000000000000use crate::*; /// A mount point #[derive(Debug, Clone)] pub struct Mount { pub info: MountInfo, pub fs_label: Option, pub disk: Option, pub stats: Result, pub uuid: Option, pub part_uuid: Option, } impl Mount { /// Return inodes information, when available and consistent pub fn inodes(&self) -> Option<&Inodes> { self.stats .as_ref() .ok() .and_then(|stats| stats.inodes.as_ref()) } /// Return the stats, if they could be fetched and /// make sense. /// /// Most often, you don't care *why* there are no stats, /// because the error cases are mostly non storage volumes, /// so it's a best practice to no try to analyze the error /// but just use this option returning method. /// /// The most interesting case is when a network volume is /// unreachable, which you can test with is_unreachable(). pub fn stats(&self) -> Option<&Stats> { self.stats.as_ref().ok() } /// Tell whether the reason we have no stats is because the /// filesystem is unreachable pub fn is_unreachable(&self) -> bool { matches!(self.stats, Err(StatsError::Unreachable)) } /// Tell whether the reason we have no stats is because /// there was a timeout trying to fetch them pub fn is_timeout(&self) -> bool { matches!(self.stats, Err(StatsError::Timeout)) } #[cfg(unix)] pub fn is_remote(&self) -> bool { self.info.is_remote() } #[cfg(windows)] pub fn is_remote(&self) -> bool { self.disk.as_ref().is_some_and(|disk| disk.remote) } } lfs-core-0.19.2/src/mountinfo.rs000064400000000000000000000070271046102023000146100ustar 00000000000000use { crate::*, lazy_regex::*, std::path::PathBuf, }; #[cfg(unix)] static REMOTE_ONLY_FS_TYPES: &[&str] = &[ "afs", "coda", "auristorfs", "fhgfs", "gpfs", "ibrix", "ocfs2", "vxfs", ]; /// options that may be present in the options vec but that we /// don't want to see in the `options_string()` returned value static OPTIONS_NOT_IN_OPTIONS_STRING: &[&str] = &[ "removable", // parsed on mac but not found in /proc/mountinfo ]; /// An id of a mount pub type MountId = u32; /// A mount point as described in /proc/self/mountinfo #[derive(Debug, Clone)] pub struct MountInfo { pub id: Option, pub parent: Option, pub dev: DeviceId, pub root: PathBuf, pub mount_point: PathBuf, pub options: Vec, pub fs: String, // rename into "node" ? pub fs_type: String, /// whether it's a bound mount (usually mirroring part of another device) pub bound: bool, } #[derive(Debug, Clone, PartialEq)] pub struct MountOption { pub name: String, pub value: Option, } impl MountOption { pub fn new>( name: S, value: Option, ) -> Self { MountOption { name: name.into(), value: value.map(|s| s.into()), } } } impl MountInfo { /// return `` when the path is `/dev/mapper/` pub fn dm_name(&self) -> Option<&str> { regex_captures!(r#"^/dev/mapper/([^/]+)$"#, &self.fs).map(|(_, dm_name)| dm_name) } /// return the last token of the fs path pub fn fs_name(&self) -> Option<&str> { regex_find!(r#"[^\\/]+$"#, &self.fs) } /// tell whether the mount looks remote /// /// Heuristics copied from #[cfg(unix)] pub fn is_remote(&self) -> bool { self.fs.contains(':') || (self.fs.starts_with("//") && ["cifs", "smb3", "smbfs"].contains(&self.fs_type.as_ref())) || REMOTE_ONLY_FS_TYPES.contains(&self.fs_type.as_ref()) || self.fs == "-hosts" } /// return a string like "rw,noatime,compress=zstd:3,space_cache=v2,subvolid=256" /// (as in /proc/mountinfo) /// /// Some options may be skipped as they're less relevant (but you may still find them /// in the options vec) pub fn options_string(&self) -> String { let mut s = String::new(); let mut first = true; for option in &self.options { if OPTIONS_NOT_IN_OPTIONS_STRING .iter() .any(|s| s == &option.name) { continue; } if !first { s.push(','); } s.push_str(&option.name); if let Some(value) = &option.value { s.push('='); s.push_str(value); } first = false; } s } /// tell whether the option (eg "compress", "rw", "noatime") is present /// among options pub fn has_option( &self, name: &str, ) -> bool { for option in &self.options { if option.name == name { return true; } } false } /// return the value of the mountoption, or None pub fn option_value( &self, name: &str, ) -> Option<&str> { for option in &self.options { if option.name == name { return option.value.as_deref(); } } None } } lfs-core-0.19.2/src/read_options.rs000064400000000000000000000030411046102023000152500ustar 00000000000000use std::{ str::FromStr, time::Duration, }; #[derive(Debug, Clone, Copy)] #[non_exhaustive] pub enum Strategy { /// On mac, with this strategy, IOKit is called Iokit, /// On mac, with this strategy, the output of the diskutil /// command is parsed Diskutil, } #[derive(Debug, Clone, Copy)] pub struct ReadOptions { pub(crate) remote_stats: bool, pub(crate) strategy: Option, pub(crate) stats_timeout: Option, } impl Default for ReadOptions { fn default() -> Self { Self { remote_stats: true, strategy: None, stats_timeout: Some(Duration::from_millis(50)), } } } impl ReadOptions { pub fn remote_stats( mut self, v: bool, ) -> Self { self.remote_stats = v; self } pub fn strategy( mut self, v: Strategy, ) -> Self { self.strategy = Some(v); self } /// Set the timeout for reading stats on remote filesystems /// (unix only), which by default is 100ms. pub fn stats_timeout( mut self, v: Option, ) -> Self { self.stats_timeout = v; self } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParseStrategyError; impl FromStr for Strategy { type Err = ParseStrategyError; fn from_str(s: &str) -> Result { match s { "iokit" => Ok(Self::Iokit), "diskutil" => Ok(Self::Diskutil), _ => Err(ParseStrategyError), } } } lfs-core-0.19.2/src/stats/mod.rs000064400000000000000000000010141046102023000144750ustar 00000000000000#[cfg(unix)] mod unix; #[cfg(windows)] mod windows; #[cfg(unix)] pub use unix::*; #[cfg(windows)] pub use windows::*; #[derive(Debug, snafu::Snafu, Clone, Copy, PartialEq, Eq)] #[snafu(visibility(pub(crate)))] pub enum StatsError { #[snafu(display("Could not stat mount point"))] Unreachable, #[snafu(display("Timeout on stating mount point"))] Timeout, #[snafu(display("Unconsistent stats"))] Unconsistent, /// Options made us not even try #[snafu(display("Excluded"))] Excluded, } lfs-core-0.19.2/src/stats/unix.rs000064400000000000000000000020541046102023000147060ustar 00000000000000use crate::Inodes; /// inode & blocs information /// /// The semantics is mostly the one of statvfs, with addition of /// bused which is necessary for volumes freely growing in containers #[derive(Debug, Clone)] pub struct Stats { /// block size pub bsize: u64, /// number of blocks pub blocks: u64, /// not provided by statvfs pub bused: u64, /// number of free blocks pub bfree: u64, /// number of free blocks for underprivileged users pub bavail: u64, /// information relative to inodes, if available pub inodes: Option, } impl Stats { pub fn size(&self) -> u64 { self.bsize * self.blocks } pub fn available(&self) -> u64 { self.bsize * self.bavail } /// Space used in the volume (including unreadable fs metadata) pub fn used(&self) -> u64 { self.bsize * self.bused } pub fn use_share(&self) -> f64 { if self.blocks == 0 { 0.0 } else { (self.blocks - self.bfree) as f64 / self.blocks as f64 } } } lfs-core-0.19.2/src/stats/windows.rs000064400000000000000000000013241046102023000154140ustar 00000000000000use crate::Inodes; /// inode & storage usage information #[derive(Debug, Clone)] pub struct Stats { /// number of bytes pub size: u64, /// number of free bytes pub free: u64, /// information relative to inodes, if available pub inodes: Option, } impl Stats { pub fn size(&self) -> u64 { self.size } pub fn available(&self) -> u64 { self.free } /// Space used in the volume (including unreadable fs metadata) pub fn used(&self) -> u64 { self.size - self.free } pub fn use_share(&self) -> f64 { if self.free == 0 { 0.0 } else { (self.size - self.free) as f64 / self.size as f64 } } } lfs-core-0.19.2/src/sys.rs000064400000000000000000000023111046102023000133770ustar 00000000000000use std::{ fs::File, io::{ self, Read, }, path::Path, }; /// read a system file into a string #[allow(dead_code)] pub fn read_file>(path: P) -> io::Result { let mut file = File::open(path.as_ref())?; let mut buf = String::new(); file.read_to_string(&mut buf)?; Ok(buf) } /// read a system file into a boolean (assuming "0" or "1") #[cfg(target_os = "linux")] pub fn read_file_as_bool>(path: P) -> Option { read_file(path).ok().and_then(|c| match c.trim() { "0" => Some(false), "1" => Some(true), _ => None, }) } /// decode ascii-octal or ascii-hexa encoded strings #[cfg(target_os = "linux")] pub fn decode_string>(s: S) -> String { use lazy_regex::*; // replacing octal escape sequences let s = regex_replace_all!(r#"\\0(\d\d)"#, s.as_ref(), |_, n: &str| { let c = u8::from_str_radix(n, 8).unwrap() as char; c.to_string() }); // replacing hexa escape sequences let s = regex_replace_all!(r#"\\x([0-9a-fA-F]{2})"#, &s, |_, n: &str| { let c = u8::from_str_radix(n, 16).unwrap() as char; c.to_string() }); s.to_string() } lfs-core-0.19.2/src/windows/mod.rs000064400000000000000000000026021046102023000150350ustar 00000000000000mod volume; use { crate::{ Error, Mount, ReadOptions, WindowsApiSnafu, windows::volume::get_volumes, }, ::snafu::prelude::*, std::{ os::windows::ffi::OsStrExt, path::Path, }, windows::{ Win32::Storage::FileSystem::GetVolumeInformationW, core::PCWSTR, }, }; /// Read all the mount points and load basic information on them pub fn read_mounts(options: &ReadOptions) -> Result, Error> { Ok(get_volumes()? .into_iter() .flat_map(|volume| volume.to_dysk_mounts(options).ok()) .flatten() .collect()) } /// Get a volume serial number for a provided root path /// /// Call pub fn volume_serial_for_path(path: impl AsRef) -> Result { let path_wide: Vec = path .as_ref() .as_os_str() .encode_wide() .chain(std::iter::once(0)) .collect(); let mut serial_number: u32 = 0; unsafe { GetVolumeInformationW( PCWSTR(path_wide.as_ptr()), None, Some(&mut serial_number), None, None, None, ) .context(WindowsApiSnafu { api: "GetVolumeInformationW", })?; } Ok(serial_number) } lfs-core-0.19.2/src/windows/volume/volume_utils.rs000064400000000000000000000063661046102023000203270ustar 00000000000000use { crate::windows::volume::{ VolumeKind, VolumeName, }, std::{ self, ptr, }, windows::{ Win32::{ Foundation::{ ERROR_MORE_DATA, HANDLE, }, Storage::FileSystem::{ BusTypeSpaces, CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, OPEN_EXISTING, }, System::{ IO::DeviceIoControl, Ioctl::{ DISK_EXTENT, IOCTL_STORAGE_QUERY_PROPERTY, PropertyStandardQuery, STORAGE_DEVICE_DESCRIPTOR, STORAGE_PROPERTY_QUERY, StorageDeviceProperty, VOLUME_DISK_EXTENTS, }, }, }, core::Owned, }, }; pub fn volume_kind_detect(verbatim_path: &VolumeName) -> VolumeKind { let handle = match unsafe { CreateFileW( verbatim_path.as_pcwstr_no_trailing_backslash(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, Default::default(), None, ) } { Ok(handle) => handle, Err(_) => return VolumeKind::Unknown, }; let handle = unsafe { Owned::new(handle) }; let mut extents_buffer = VOLUME_DISK_EXTENTS { NumberOfDiskExtents: 0, Extents: [DISK_EXTENT { DiskNumber: 0, StartingOffset: 0, ExtentLength: 0, }], }; let mut bytes_returned: u32 = 0; let result = unsafe { DeviceIoControl( *handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, None, 0, Some(&mut extents_buffer as *mut _ as *mut _), std::mem::size_of::() as u32, Some(&mut bytes_returned), None, ) }; match result { Ok(_) => match extents_buffer.NumberOfDiskExtents { 1 if is_volume_storage_space(*handle) => VolumeKind::StorageSpace, 1 => VolumeKind::Simple { disk_number: extents_buffer.Extents[0].DiskNumber, }, _ => VolumeKind::DynamicDisk, }, Err(error) if error.code() == ERROR_MORE_DATA.to_hresult() => VolumeKind::DynamicDisk, Err(_) => VolumeKind::Unknown, } } fn is_volume_storage_space(handle: HANDLE) -> bool { let query = STORAGE_PROPERTY_QUERY { PropertyId: StorageDeviceProperty, QueryType: PropertyStandardQuery, AdditionalParameters: [0; 1], }; let mut descriptor = STORAGE_DEVICE_DESCRIPTOR::default(); let result = unsafe { DeviceIoControl( handle, IOCTL_STORAGE_QUERY_PROPERTY, Some(ptr::addr_of!(query).cast()), std::mem::size_of::() as u32, Some(ptr::addr_of_mut!(descriptor).cast()), std::mem::size_of::() as u32, None, None, ) }; result.is_ok_and(|_| descriptor.BusType == BusTypeSpaces) } lfs-core-0.19.2/src/windows/volume.rs000064400000000000000000000304661046102023000155760ustar 00000000000000use { crate::{ DeviceId, Disk, Mount, MountInfo, ReadOptions, Stats, StatsError, WindowsApiSnafu, windows::volume::volume_utils::volume_kind_detect, }, snafu::{ ResultExt, prelude::*, }, std::{ ffi::OsString, fmt, os::windows::ffi::OsStringExt, path::PathBuf, ptr, }, windows::{ Win32::{ Foundation::{ CloseHandle, ERROR_MORE_DATA, HANDLE, MAX_PATH, }, Storage::FileSystem::{ CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDiskFreeSpaceExW, GetDriveTypeW, GetVolumeInformationW, GetVolumePathNamesForVolumeNameW, OPEN_EXISTING, }, System::{ IO::DeviceIoControl, Ioctl::{ DEVICE_SEEK_PENALTY_DESCRIPTOR, IOCTL_STORAGE_QUERY_PROPERTY, PropertyStandardQuery, STORAGE_PROPERTY_QUERY, StorageDeviceSeekPenaltyProperty, }, SystemServices::FILE_READ_ONLY_VOLUME, WindowsProgramming::{ DRIVE_CDROM, DRIVE_FIXED, DRIVE_RAMDISK, DRIVE_REMOTE, DRIVE_REMOVABLE, }, }, }, core::PCWSTR, }, }; mod volume_utils; #[derive(Debug, Clone)] pub enum VolumeKind { Simple { disk_number: u32 }, DynamicDisk, StorageSpace, Unknown, } trait WideStringExt { fn wcslen(&self) -> usize; } impl WideStringExt for [u16] { fn wcslen(&self) -> usize { self.iter().position(|&c| c == 0).unwrap_or(self.len()) } } #[derive(Debug, Snafu)] #[snafu(display("Invalid volume name: {:?}", volume_name))] pub struct VolumeNameError { volume_name: OsString, } pub struct VolumeName { full_path: Vec, device_path: Vec, } impl fmt::Debug for VolumeName { fn fmt( &self, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { let s = String::from_utf16_lossy(&self.full_path[..self.full_path.wcslen()]); f.debug_tuple("VolumeName").field(&s).finish() } } impl fmt::Display for VolumeName { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { let s = String::from_utf16_lossy(&self.full_path[..self.full_path.wcslen()]); write!(f, "{}", s) } } impl VolumeName { const PREFIX: &[u16] = &[ b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16, b'V' as u16, b'o' as u16, b'l' as u16, b'u' as u16, b'm' as u16, b'e' as u16, b'{' as u16, ]; const SUFFIX: &[u16] = &[b'}' as u16, b'\\' as u16, 0]; pub fn from_null_terminated(buffer: &[u16]) -> Result { let length = buffer.wcslen(); let full_path = &buffer[..=length]; if !buffer.starts_with(Self::PREFIX) || !buffer[..=length].ends_with(Self::SUFFIX) { return Err(VolumeNameError { volume_name: OsString::from_wide(full_path), }); } let mut device_path = full_path[..full_path.len() - 1].to_vec(); if let Some(last) = device_path.last_mut() { *last = 0; } Ok(VolumeName { full_path: full_path.to_vec(), device_path, }) } pub fn to_uuid(&self) -> Option { let uuid = &self.full_path[Self::PREFIX.len()..self.full_path.len() - Self::SUFFIX.len()]; String::from_utf16(uuid).ok() } pub fn as_pcwstr(&self) -> PCWSTR { PCWSTR(self.full_path.as_ptr()) } pub fn as_pcwstr_no_trailing_backslash(&self) -> PCWSTR { PCWSTR(self.device_path.as_ptr()) } } #[derive(Debug)] struct VolumeInformation { label: String, serial_number: u32, read_only: bool, file_system_name: String, } #[derive(Debug)] pub struct Volume { name: VolumeName, } impl Volume { pub fn new(name: VolumeName) -> Self { Self { name } } pub fn to_dysk_mounts( &self, options: &ReadOptions, ) -> Result, crate::Error> { let mounts = self.mount_points()?; if mounts.is_empty() { return Ok(Vec::new()); } let disk = self.disk_info(); let stats = if !options.remote_stats && disk.as_ref().is_some_and(|disk| disk.remote) { Err(StatsError::Excluded) } else { self.volume_stats() }; let VolumeInformation { serial_number, label, file_system_name, .. } = self.volume_information()?; Ok(mounts .into_iter() .map(|mount_point| { let info = MountInfo { id: None, parent: None, dev: DeviceId::from(serial_number), root: mount_point.clone(), mount_point, options: Vec::new(), fs: self.name.to_string(), fs_type: file_system_name.clone(), bound: false, }; Mount { info, fs_label: Some(label.clone()), disk: disk.clone(), stats: stats.clone(), uuid: self.name.to_uuid(), part_uuid: None, } }) .collect()) } fn mount_points(&self) -> Result, crate::Error> { let mut char_count = MAX_PATH + 1; loop { let mut mounts = vec![0u16; char_count as usize]; match unsafe { GetVolumePathNamesForVolumeNameW( self.name.as_pcwstr(), Some(&mut mounts), &mut char_count, ) } { Ok(_) => { return Ok(mounts[..char_count as usize] .split(|&c| c == 0) .filter(|s| !s.is_empty()) .map(OsString::from_wide) .map(PathBuf::from) .collect()); } Err(error) if error.code() == ERROR_MORE_DATA.to_hresult() => continue, Err(error) => { return Err(error).context(WindowsApiSnafu { api: "GetVolumePathNamesForVolumeNameW", }); } } } } fn volume_information(&self) -> Result { // The max supported buffer size for GetVolumeInformationW const BUFFER_SIZE: usize = (MAX_PATH + 1) as usize; let mut serial_number: u32 = 0; let mut flags: u32 = 0; let mut volume_label_buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE]; let mut file_system_name_buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE]; unsafe { GetVolumeInformationW( self.name.as_pcwstr(), Some(&mut volume_label_buffer), Some(&mut serial_number), None, Some(&mut flags), Some(&mut file_system_name_buffer), ) .context(WindowsApiSnafu { api: "GetVolumeInformationW", })?; } Ok(VolumeInformation { label: String::from_utf16_lossy(&volume_label_buffer[..volume_label_buffer.wcslen()]), serial_number, read_only: flags & FILE_READ_ONLY_VOLUME != 0, file_system_name: String::from_utf16_lossy( &file_system_name_buffer[..file_system_name_buffer.wcslen()], ), }) } fn disk_info(&self) -> Option { let volume_kind = self.volume_kind(); let drive_type = unsafe { GetDriveTypeW(self.name.as_pcwstr()) }; let (removable, remote, ram) = match drive_type { DRIVE_REMOVABLE => (Some(true), false, false), DRIVE_FIXED => (Some(false), false, false), DRIVE_REMOTE => (Some(false), true, false), DRIVE_CDROM => (Some(true), false, false), DRIVE_RAMDISK => (Some(false), false, true), _ => return None, }; let rotational = if let VolumeKind::Simple { disk_number } = volume_kind { is_disk_rotational(disk_number) } else { None }; Some(Disk { rotational, removable, read_only: self.volume_information().map(|info| info.read_only).ok(), ram, image: false, lvm: matches!(volume_kind, VolumeKind::DynamicDisk) || matches!(volume_kind, VolumeKind::StorageSpace), crypted: false, remote, }) } fn volume_kind(&self) -> VolumeKind { let kind = volume_kind_detect(&self.name); // only pollute stderr in debug builds #[cfg(debug_assertions)] dbg!(&kind); kind } fn volume_stats(&self) -> Result { let mut free_bytes_available: u64 = 0; let mut total_bytes: u64 = 0; let mut total_free_bytes: u64 = 0; unsafe { GetDiskFreeSpaceExW( self.name.as_pcwstr(), Some(ptr::addr_of_mut!(free_bytes_available).cast()), Some(ptr::addr_of_mut!(total_bytes).cast()), Some(ptr::addr_of_mut!(total_free_bytes).cast()), ) .map_err(|_| StatsError::Unreachable)?; } Ok(Stats { size: total_bytes, free: free_bytes_available, inodes: None, }) } } pub fn get_volumes() -> Result, crate::Error> { let mut volume_names = Vec::new(); let mut volume_name_buffer: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; let handle: HANDLE = unsafe { FindFirstVolumeW(&mut volume_name_buffer).context(WindowsApiSnafu { api: "FindFirstVolumeW", })? }; loop { if let Ok(name) = VolumeName::from_null_terminated(&volume_name_buffer) { volume_names.push(name); }; if unsafe { FindNextVolumeW(handle, &mut volume_name_buffer).is_err() } { // Break not return so that the handle can be closed break; } } unsafe { FindVolumeClose(handle).context(WindowsApiSnafu { api: "FindVolumeClose", })? }; Ok(volume_names.into_iter().map(Volume::new).collect()) } fn is_disk_rotational(disk_number: u32) -> Option { let path: Vec = format!("\\\\.\\PhysicalDrive{}\0", disk_number) .encode_utf16() .collect(); let handle = match unsafe { CreateFileW( PCWSTR(path.as_ptr()), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, Default::default(), None, ) } { Ok(handle) => handle, Err(_) => return None, }; let query = STORAGE_PROPERTY_QUERY { PropertyId: StorageDeviceSeekPenaltyProperty, QueryType: PropertyStandardQuery, AdditionalParameters: [0; 1], }; let mut seek_penalty = DEVICE_SEEK_PENALTY_DESCRIPTOR { Version: 0, Size: 0, IncursSeekPenalty: false, }; let mut bytes_returned = 0u32; let result = unsafe { DeviceIoControl( handle, IOCTL_STORAGE_QUERY_PROPERTY, Some(ptr::addr_of!(query).cast()), std::mem::size_of::() as u32, Some(ptr::addr_of_mut!(seek_penalty).cast()), std::mem::size_of::() as u32, Some(&mut bytes_returned), None, ) }; let _ = unsafe { CloseHandle(handle) }; match result { Ok(_) => Some(seek_penalty.IncursSeekPenalty), _ => None, } }