Skip to content

Commit 81ee8d1

Browse files
authored
Merge pull request #9078 from Alonely0/stat_fix
fix(stat): Collection of fixes for stat(1)
2 parents 3df9444 + 613e9be commit 81ee8d1

File tree

6 files changed

+134
-43
lines changed

6 files changed

+134
-43
lines changed

.vscode/cspell.dictionaries/jargon.wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ listxattr
8686
llistxattr
8787
lossily
8888
lstat
89+
makedev
8990
mebi
9091
mebibytes
9192
mergeable

src/uu/mknod/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ path = "src/mknod.rs"
2121
[dependencies]
2222
clap = { workspace = true }
2323
libc = { workspace = true }
24-
uucore = { workspace = true, features = ["mode"] }
24+
uucore = { workspace = true, features = ["mode", "fs"] }
2525
fluent = { workspace = true }
2626

2727
[features]

src/uu/mknod/src/mknod.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::ffi::CString;
1313
use uucore::display::Quotable;
1414
use uucore::error::{UResult, USimpleError, UUsageError, set_exit_code};
1515
use uucore::format_usage;
16+
use uucore::fs::makedev;
1617
use uucore::translate;
1718

1819
const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
@@ -26,12 +27,6 @@ mod options {
2627
pub const CONTEXT: &str = "context";
2728
}
2829

29-
#[inline(always)]
30-
fn makedev(maj: u64, min: u64) -> dev_t {
31-
// pick up from <sys/sysmacros.h>
32-
((min & 0xff) | ((maj & 0xfff) << 8) | ((min & !0xff) << 12) | ((maj & !0xfff) << 32)) as dev_t
33-
}
34-
3530
#[derive(Clone, PartialEq)]
3631
enum FileType {
3732
Block,
@@ -145,7 +140,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
145140
translate!("mknod-error-fifo-no-major-minor"),
146141
));
147142
}
148-
(_, Some(&major), Some(&minor)) => makedev(major, minor),
143+
(_, Some(&major), Some(&minor)) => makedev(major as _, minor as _),
149144
_ => {
150145
return Err(UUsageError::new(
151146
1,

src/uu/stat/src/stat.rs

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use uucore::translate;
99

1010
use clap::builder::ValueParser;
1111
use uucore::display::Quotable;
12-
use uucore::fs::display_permissions;
12+
use uucore::fs::{display_permissions, major, minor};
1313
use uucore::fsext::{
1414
FsMeta, MetadataTimeField, StatFs, metadata_get_time, pretty_filetype, pretty_fstype,
1515
read_fs_list, statfs,
@@ -70,6 +70,8 @@ struct Flags {
7070
space: bool,
7171
sign: bool,
7272
group: bool,
73+
major: bool,
74+
minor: bool,
7375
}
7476

7577
/// checks if the string is within the specified bound,
@@ -739,7 +741,6 @@ impl Stater {
739741
return Ok(Token::Char('%'));
740742
}
741743
if chars[*i] == '%' {
742-
*i += 1;
743744
return Ok(Token::Char('%'));
744745
}
745746

@@ -794,13 +795,14 @@ impl Stater {
794795
if let Some(&next_char) = chars.get(*i + 1) {
795796
if (chars[*i] == 'H' || chars[*i] == 'L') && (next_char == 'd' || next_char == 'r')
796797
{
797-
let specifier = format!("{}{next_char}", chars[*i]);
798+
flag.major = chars[*i] == 'H';
799+
flag.minor = chars[*i] == 'L';
798800
*i += 1;
799801
return Ok(Token::Directive {
800802
flag,
801803
width,
802804
precision,
803-
format: specifier.chars().next().unwrap(),
805+
format: next_char,
804806
});
805807
}
806808
}
@@ -908,6 +910,28 @@ impl Stater {
908910
Ok(tokens)
909911
}
910912

913+
fn populate_mount_list() -> UResult<Vec<OsString>> {
914+
let mut mount_list = read_fs_list()
915+
.map_err(|e| {
916+
USimpleError::new(
917+
e.code(),
918+
StatError::CannotReadFilesystem {
919+
error: e.to_string(),
920+
}
921+
.to_string(),
922+
)
923+
})?
924+
.iter()
925+
.map(|mi| mi.mount_dir.clone())
926+
.collect::<Vec<_>>();
927+
928+
// Reverse sort. The longer comes first.
929+
mount_list.sort();
930+
mount_list.reverse();
931+
932+
Ok(mount_list)
933+
}
934+
911935
fn new(matches: &ArgMatches) -> UResult<Self> {
912936
let files: Vec<OsString> = matches
913937
.get_many::<OsString>(options::FILES)
@@ -938,27 +962,16 @@ impl Stater {
938962
let default_dev_tokens =
939963
Self::generate_tokens(&Self::default_format(show_fs, terse, true), use_printf)?;
940964

941-
let mount_list = if show_fs {
942-
// mount points aren't displayed when showing filesystem information
965+
// mount points aren't displayed when showing filesystem information, or
966+
// whenever the format string does not request the mount point.
967+
let mount_list = if show_fs
968+
|| !default_tokens
969+
.iter()
970+
.any(|tok| matches!(tok, Token::Directive { format: 'm', .. }))
971+
{
943972
None
944973
} else {
945-
let mut mount_list = read_fs_list()
946-
.map_err(|e| {
947-
USimpleError::new(
948-
e.code(),
949-
StatError::CannotReadFilesystem {
950-
error: e.to_string(),
951-
}
952-
.to_string(),
953-
)
954-
})?
955-
.iter()
956-
.map(|mi| mi.mount_dir.clone())
957-
.collect::<Vec<_>>();
958-
// Reverse sort. The longer comes first.
959-
mount_list.sort();
960-
mount_list.reverse();
961-
Some(mount_list)
974+
Some(Self::populate_mount_list()?)
962975
};
963976

964977
Ok(Self {
@@ -1052,6 +1065,8 @@ impl Stater {
10521065
}
10531066
}
10541067
// device number in decimal
1068+
'd' if flag.major => OutputType::Unsigned(major(meta.dev() as _) as u64),
1069+
'd' if flag.minor => OutputType::Unsigned(minor(meta.dev() as _) as u64),
10551070
'd' => OutputType::Unsigned(meta.dev()),
10561071
// device number in hex
10571072
'D' => OutputType::UnsignedHex(meta.dev()),
@@ -1090,10 +1105,10 @@ impl Stater {
10901105
's' => OutputType::Integer(meta.len() as i64),
10911106
// major device type in hex, for character/block device special
10921107
// files
1093-
't' => OutputType::UnsignedHex(meta.rdev() >> 8),
1108+
't' => OutputType::UnsignedHex(major(meta.rdev() as _) as u64),
10941109
// minor device type in hex, for character/block device special
10951110
// files
1096-
'T' => OutputType::UnsignedHex(meta.rdev() & 0xff),
1111+
'T' => OutputType::UnsignedHex(minor(meta.rdev() as _) as u64),
10971112
// user ID of owner
10981113
'u' => OutputType::Unsigned(meta.uid() as u64),
10991114
// user name of owner
@@ -1136,15 +1151,10 @@ impl Stater {
11361151
.map_or((0, 0), system_time_to_sec);
11371152
OutputType::Float(sec as f64 + nsec as f64 / 1_000_000_000.0)
11381153
}
1139-
'R' => {
1140-
let major = meta.rdev() >> 8;
1141-
let minor = meta.rdev() & 0xff;
1142-
OutputType::Str(format!("{major},{minor}"))
1143-
}
1154+
'R' => OutputType::UnsignedHex(meta.rdev()),
1155+
'r' if flag.major => OutputType::Unsigned(major(meta.rdev() as _) as u64),
1156+
'r' if flag.minor => OutputType::Unsigned(minor(meta.rdev() as _) as u64),
11441157
'r' => OutputType::Unsigned(meta.rdev()),
1145-
'H' => OutputType::Unsigned(meta.rdev() >> 8), // Major in decimal
1146-
'L' => OutputType::Unsigned(meta.rdev() & 0xff), // Minor in decimal
1147-
11481158
_ => OutputType::Unknown,
11491159
};
11501160
print_it(&output, flag, width, precision);
@@ -1269,7 +1279,7 @@ impl Stater {
12691279
} else {
12701280
let device_line = if show_dev_type {
12711281
format!(
1272-
"{}: %Dh/%dd\t{}: %-10i {}: %-5h {} {}: %t,%T\n",
1282+
"{}: %Hd,%Ld\t{}: %-10i {}: %-5h {} {}: %t,%T\n",
12731283
translate!("stat-word-device"),
12741284
translate!("stat-word-inode"),
12751285
translate!("stat-word-links"),
@@ -1278,7 +1288,7 @@ impl Stater {
12781288
)
12791289
} else {
12801290
format!(
1281-
"{}: %Dh/%dd\t{}: %-10i {}: %h\n",
1291+
"{}: %Hd,%Ld\t{}: %-10i {}: %h\n",
12821292
translate!("stat-word-device"),
12831293
translate!("stat-word-inode"),
12841294
translate!("stat-word-links")

src/uucore/src/lib/features/fs.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use libc::{
1313
S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR,
1414
mkfifo, mode_t,
1515
};
16+
#[cfg(all(unix, not(target_os = "redox")))]
17+
pub use libc::{major, makedev, minor};
1618
use std::collections::HashSet;
1719
use std::collections::VecDeque;
1820
use std::env;
@@ -839,6 +841,24 @@ pub fn make_fifo(path: &Path) -> std::io::Result<()> {
839841
}
840842
}
841843

844+
// Redox's libc appears not to include the following utilities
845+
846+
#[cfg(target_os = "redox")]
847+
pub fn major(dev: libc::dev_t) -> libc::c_uint {
848+
(((dev >> 8) & 0xFFF) | ((dev >> 32) & 0xFFFFF000)) as _
849+
}
850+
851+
#[cfg(target_os = "redox")]
852+
pub fn minor(dev: libc::dev_t) -> libc::c_uint {
853+
((dev & 0xFF) | ((dev >> 12) & 0xFFFFF00)) as _
854+
}
855+
856+
#[cfg(target_os = "redox")]
857+
pub fn makedev(maj: libc::c_uint, min: libc::c_uint) -> libc::dev_t {
858+
let [maj, min] = [maj as libc::dev_t, min as libc::dev_t];
859+
(min & 0xff) | ((maj & 0xfff) << 8) | ((min & !0xff) << 12) | ((maj & !0xfff) << 32)
860+
}
861+
842862
#[cfg(test)]
843863
mod tests {
844864
// Note this useful idiom: importing names from outer (for mod tests) scope.

tests/by-util/test_stat.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use uutests::unwrap_or_return;
99
use uutests::util::{TestScenario, expected_result};
1010
use uutests::util_name;
1111

12+
use std::fs::metadata;
13+
use std::os::unix::fs::MetadataExt;
14+
1215
#[test]
1316
fn test_invalid_arg() {
1417
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
@@ -567,3 +570,65 @@ fn test_mount_point_combined_with_other_specifiers() {
567570
"Should print mount point, file name, and size"
568571
);
569572
}
573+
574+
#[cfg(unix)]
575+
#[test]
576+
fn test_percent_escaping() {
577+
let ts = TestScenario::new(util_name!());
578+
let result = ts
579+
.ucmd()
580+
.args(&["--printf", "%%%m%%m%m%%%", "/bin/sh"])
581+
.succeeds();
582+
assert_eq!(result.stdout_str(), "%/%m/%%");
583+
}
584+
585+
#[cfg(unix)]
586+
#[test]
587+
fn test_correct_metadata() {
588+
use uucore::fs::{major, minor};
589+
590+
let ts = TestScenario::new(util_name!());
591+
let parse = |(i, str): (usize, &str)| {
592+
// Some outputs (%[fDRtT]) are in hex; they're redundant, but we might
593+
// as well also test case conversion.
594+
let radix = if matches!(i, 2 | 10 | 14..) { 16 } else { 10 };
595+
i128::from_str_radix(str, radix)
596+
};
597+
for device in ["/", "/dev/null"] {
598+
let metadata = metadata(device).unwrap();
599+
// We avoid time vals because of fs race conditions, especially with
600+
// access time and status time (this previously killed an otherwise
601+
// perfect 11-hour-long CI run...). The large number of as-casts is
602+
// due to inconsistencies on some platforms (read: BSDs), and we use
603+
// i128 as a lowest-common denominator.
604+
let test_str = "%u %g %f %b %s %h %i %d %Hd %Ld %D %r %Hr %Lr %R %t %T";
605+
let expected = [
606+
metadata.uid() as _,
607+
metadata.gid() as _,
608+
metadata.mode() as _,
609+
metadata.blocks() as _,
610+
metadata.size() as _,
611+
metadata.nlink() as _,
612+
metadata.ino() as _,
613+
metadata.dev() as _,
614+
major(metadata.dev() as _) as _,
615+
minor(metadata.dev() as _) as _,
616+
metadata.dev() as _,
617+
metadata.rdev() as _,
618+
major(metadata.rdev() as _) as _,
619+
minor(metadata.rdev() as _) as _,
620+
metadata.rdev() as _,
621+
major(metadata.rdev() as _) as _,
622+
minor(metadata.rdev() as _) as _,
623+
];
624+
let result = ts.ucmd().args(&["--printf", test_str, device]).succeeds();
625+
let output = result
626+
.stdout_str()
627+
.split(' ')
628+
.enumerate()
629+
.map(parse)
630+
.collect::<Result<Vec<i128>, _>>()
631+
.unwrap();
632+
assert_eq!(output, &expected);
633+
}
634+
}

0 commit comments

Comments
 (0)