Skip to content

Commit bc66596

Browse files
authored
Merge pull request #249 from cakebaker/pmap_device_format
pmap: implement `--device`
2 parents 087d37c + cc6d948 commit bc66596

File tree

3 files changed

+212
-35
lines changed

3 files changed

+212
-35
lines changed

src/uu/pmap/src/maps_format_parser.rs

Lines changed: 98 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,59 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6+
use std::fmt;
67
use std::io::{Error, ErrorKind};
78

8-
// Represents a parsed single line from /proc/<PID>/maps.
9+
// Represents a parsed single line from /proc/<PID>/maps for the default and device formats. It
10+
// omits the inode information because it's not used by those formats.
911
#[derive(Debug, PartialEq)]
1012
pub struct MapLine {
1113
pub address: String,
1214
pub size_in_kb: u64,
13-
pub perms: String,
15+
pub perms: Perms,
16+
pub offset: String,
17+
pub device: String,
1418
pub mapping: String,
1519
}
1620

21+
// Represents a set of permissions from the "perms" column of /proc/<PID>/maps.
22+
#[derive(Clone, Copy, Debug, PartialEq)]
23+
pub struct Perms {
24+
pub readable: bool,
25+
pub writable: bool,
26+
pub executable: bool,
27+
pub shared: bool,
28+
}
29+
30+
impl From<&str> for Perms {
31+
fn from(s: &str) -> Self {
32+
let mut chars = s.chars();
33+
34+
Self {
35+
readable: chars.next() == Some('r'),
36+
writable: chars.next() == Some('w'),
37+
executable: chars.next() == Some('x'),
38+
shared: chars.next() == Some('s'),
39+
}
40+
}
41+
}
42+
43+
// Please note: While `Perms` has four boolean fields, it's string representation has five
44+
// characters because pmap's default and device formats use five characters for the perms,
45+
// with the last character always being '-'.
46+
impl fmt::Display for Perms {
47+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48+
write!(
49+
f,
50+
"{}{}{}{}-",
51+
if self.readable { 'r' } else { '-' },
52+
if self.writable { 'w' } else { '-' },
53+
if self.executable { 'x' } else { '-' },
54+
if self.shared { 's' } else { '-' },
55+
)
56+
}
57+
}
58+
1759
// Parses a single line from /proc/<PID>/maps. See
1860
// https://www.kernel.org/doc/html/latest/filesystems/proc.html for details about the expected
1961
// format.
@@ -30,16 +72,29 @@ pub fn parse_map_line(line: &str) -> Result<MapLine, Error> {
3072
let (perms, rest) = rest
3173
.split_once(' ')
3274
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
33-
let perms = parse_perms(perms);
75+
let perms = Perms::from(perms);
76+
77+
let (offset, rest) = rest
78+
.split_once(' ')
79+
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
80+
let offset = format!("{offset:0>16}");
81+
82+
let (device, rest) = rest
83+
.split_once(' ')
84+
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
85+
let device = parse_device(device)?;
3486

35-
let mapping: String = rest.splitn(4, ' ').skip(3).collect();
87+
// skip the "inode" column
88+
let mapping: String = rest.splitn(2, ' ').skip(1).collect();
3689
let mapping = mapping.trim_ascii_start();
3790
let mapping = parse_mapping(mapping);
3891

3992
Ok(MapLine {
4093
address,
4194
size_in_kb,
4295
perms,
96+
offset,
97+
device,
4398
mapping,
4499
})
45100
}
@@ -58,13 +113,12 @@ fn parse_address(memory_range: &str) -> Result<(String, u64), Error> {
58113
Ok((format!("{start:0>16}"), size_in_kb))
59114
}
60115

61-
// Turns a 4-char perms string from /proc/<PID>/maps into a 5-char perms string. The first three
62-
// chars are left untouched.
63-
fn parse_perms(perms: &str) -> String {
64-
let perms = perms.replace("p", "-");
65-
66-
// the fifth char seems to be always '-' in the original pmap
67-
format!("{perms}-")
116+
// Pads the device info from /proc/<PID>/maps with zeros and turns AB:CD into 0AB:000CD.
117+
fn parse_device(device: &str) -> Result<String, Error> {
118+
let (major, minor) = device
119+
.split_once(':')
120+
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
121+
Ok(format!("{major:0>3}:{minor:0>5}"))
68122
}
69123

70124
fn parse_mapping(mapping: &str) -> String {
@@ -86,44 +140,60 @@ fn parse_mapping(mapping: &str) -> String {
86140
mod test {
87141
use super::*;
88142

89-
fn create_map_line(address: &str, size_in_kb: u64, perms: &str, mapping: &str) -> MapLine {
143+
fn create_map_line(
144+
address: &str,
145+
size_in_kb: u64,
146+
perms: Perms,
147+
offset: &str,
148+
device: &str,
149+
mapping: &str,
150+
) -> MapLine {
90151
MapLine {
91152
address: address.to_string(),
92153
size_in_kb,
93-
perms: perms.to_string(),
154+
perms,
155+
offset: offset.to_string(),
156+
device: device.to_string(),
94157
mapping: mapping.to_string(),
95158
}
96159
}
97160

161+
#[test]
162+
fn test_perms_to_string() {
163+
assert_eq!("-----", Perms::from("---p").to_string());
164+
assert_eq!("---s-", Perms::from("---s").to_string());
165+
assert_eq!("rwx--", Perms::from("rwxp").to_string());
166+
}
167+
98168
#[test]
99169
fn test_parse_map_line() {
100170
let data = [
101171
(
102-
create_map_line("000062442eb9e000", 16, "r----", "konsole"),
172+
create_map_line("000062442eb9e000", 16, Perms::from("r--p"), "0000000000000000", "008:00008", "konsole"),
103173
"62442eb9e000-62442eba2000 r--p 00000000 08:08 10813151 /usr/bin/konsole"
104174
),
105175
(
106-
create_map_line("000071af50000000", 132, "rw---", " [ anon ]"),
176+
create_map_line("000071af50000000", 132, Perms::from("rw-p"), "0000000000000000", "000:00000", " [ anon ]"),
107177
"71af50000000-71af50021000 rw-p 00000000 00:00 0 "
108178
),
109179
(
110-
create_map_line("00007ffc3f8df000", 132, "rw---", " [ stack ]"),
180+
create_map_line("00007ffc3f8df000", 132, Perms::from("rw-p"), "0000000000000000", "000:00000", " [ stack ]"),
111181
"7ffc3f8df000-7ffc3f900000 rw-p 00000000 00:00 0 [stack]"
112182
),
113183
(
114-
create_map_line("000071af8c9e6000", 16, "rw-s-", " [ anon ]"),
184+
create_map_line("000071af8c9e6000", 16, Perms::from("rw-s"), "0000000105830000", "000:00010", " [ anon ]"),
115185
"71af8c9e6000-71af8c9ea000 rw-s 105830000 00:10 1075 anon_inode:i915.gem"
116186
),
117187
(
118-
create_map_line("000071af6cf0c000", 3560, "rw-s-", "memfd:wayland-shm (deleted)"),
188+
create_map_line("000071af6cf0c000", 3560, Perms::from("rw-s"), "0000000000000000", "000:00001", "memfd:wayland-shm (deleted)"),
119189
"71af6cf0c000-71af6d286000 rw-s 00000000 00:01 256481 /memfd:wayland-shm (deleted)"
120190
),
121191
(
122-
create_map_line("ffffffffff600000", 4, "--x--", " [ anon ]"),
192+
create_map_line("ffffffffff600000", 4, Perms::from("--xp"), "0000000000000000", "000:00000", " [ anon ]"),
123193
"ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]"
124194
),
125195
(
126-
create_map_line("00005e8187da8000", 24, "r----", "hello world"),
196+
create_map_line("00005e8187da8000", 24, Perms::from("r--p"), "0000000000000000", "008:00008", "hello world"),
127197
"5e8187da8000-5e8187dae000 r--p 00000000 08:08 9524160 /usr/bin/hello world"
128198
),
129199
];
@@ -161,10 +231,14 @@ mod test {
161231
}
162232

163233
#[test]
164-
fn test_parse_perms() {
165-
assert_eq!("-----", parse_perms("---p"));
166-
assert_eq!("---s-", parse_perms("---s"));
167-
assert_eq!("rwx--", parse_perms("rwxp"));
234+
fn test_parse_device() {
235+
assert_eq!("012:00034", parse_device("12:34").unwrap());
236+
assert_eq!("000:00000", parse_device("00:00").unwrap());
237+
}
238+
239+
#[test]
240+
fn test_parse_device_without_colon() {
241+
assert!(parse_device("1234").is_err());
168242
}
169243

170244
#[test]

src/uu/pmap/src/pmap.rs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// file that was distributed with this source code.
55

66
use clap::{crate_version, Arg, ArgAction, Command};
7-
use maps_format_parser::parse_map_line;
7+
use maps_format_parser::{parse_map_line, MapLine};
88
use std::env;
99
use std::fs;
1010
use std::io::Error;
@@ -49,11 +49,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
4949
}
5050
}
5151

52-
match parse_maps(pid) {
53-
Ok(total) => println!(" total {total:>16}K"),
54-
Err(_) => {
55-
set_exit_code(1);
56-
}
52+
if matches.get_flag(options::DEVICE) {
53+
output_device_format(pid).map_err(|_| set_exit_code(1)).ok();
54+
} else {
55+
output_default_format(pid)
56+
.map_err(|_| set_exit_code(1))
57+
.ok();
5758
}
5859
}
5960

@@ -74,21 +75,70 @@ fn parse_cmdline(pid: &str) -> Result<String, Error> {
7475
Ok(cmdline.into())
7576
}
7677

77-
fn parse_maps(pid: &str) -> Result<u64, Error> {
78+
fn process_maps<F>(pid: &str, mut process_line: F) -> Result<(), Error>
79+
where
80+
F: FnMut(&MapLine),
81+
{
7882
let path = format!("/proc/{pid}/maps");
7983
let contents = fs::read_to_string(path)?;
80-
let mut total = 0;
8184

8285
for line in contents.lines() {
8386
let map_line = parse_map_line(line)?;
87+
process_line(&map_line);
88+
}
89+
90+
Ok(())
91+
}
92+
93+
fn output_default_format(pid: &str) -> Result<(), Error> {
94+
let mut total = 0;
95+
96+
process_maps(pid, |map_line| {
8497
println!(
8598
"{} {:>6}K {} {}",
8699
map_line.address, map_line.size_in_kb, map_line.perms, map_line.mapping
87100
);
88101
total += map_line.size_in_kb;
89-
}
102+
})?;
103+
104+
println!(" total {total:>16}K");
105+
106+
Ok(())
107+
}
108+
109+
fn output_device_format(pid: &str) -> Result<(), Error> {
110+
let mut total_mapped = 0;
111+
let mut total_writeable_private = 0;
112+
let mut total_shared = 0;
113+
114+
println!("Address Kbytes Mode Offset Device Mapping");
115+
116+
process_maps(pid, |map_line| {
117+
println!(
118+
"{} {:>7} {} {} {} {}",
119+
map_line.address,
120+
map_line.size_in_kb,
121+
map_line.perms,
122+
map_line.offset,
123+
map_line.device,
124+
map_line.mapping
125+
);
126+
total_mapped += map_line.size_in_kb;
90127

91-
Ok(total)
128+
if map_line.perms.writable && !map_line.perms.shared {
129+
total_writeable_private += map_line.size_in_kb;
130+
}
131+
132+
if map_line.perms.shared {
133+
total_shared += map_line.size_in_kb;
134+
}
135+
})?;
136+
137+
println!(
138+
"mapped: {total_mapped}K writeable/private: {total_writeable_private}K shared: {total_shared}K"
139+
);
140+
141+
Ok(())
92142
}
93143

94144
pub fn uu_app() -> Command {

tests/by-util/test_pmap.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ fn test_non_existing_pid() {
7373
.no_output();
7474
}
7575

76+
#[test]
77+
#[cfg(target_os = "linux")]
78+
fn test_device() {
79+
let pid = process::id();
80+
81+
for arg in ["-d", "--device"] {
82+
let result = new_ucmd!()
83+
.arg(arg)
84+
.arg(pid.to_string())
85+
.succeeds()
86+
.stdout_move_str();
87+
88+
assert_device_format(pid, &result);
89+
}
90+
}
91+
7692
#[test]
7793
fn test_invalid_arg() {
7894
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
@@ -94,9 +110,46 @@ fn assert_format(pid: u32, s: &str) {
94110

95111
let rest = rest.trim_end();
96112
let (memory_map, last_line) = rest.rsplit_once('\n').unwrap();
97-
let re = Regex::new("(?m)^[0-9a-f]{16} +[1-9][0-9]*K (-|r)(-|w)(-|x)(-|s)- ( $$[ (anon|stack) $$]|[a-zA-Z0-9._-]+)$").unwrap();
113+
let re = Regex::new(r"(?m)^[0-9a-f]{16} +[1-9][0-9]*K (-|r)(-|w)(-|x)(-|s)- ( \[ (anon|stack) \]|[a-zA-Z0-9._-]+)$").unwrap();
98114
assert!(re.is_match(memory_map));
99115

100116
let re = Regex::new("^ total +[1-9][0-9]*K$").unwrap();
101117
assert!(re.is_match(last_line));
102118
}
119+
120+
// Ensure `s` has the following device format (--device):
121+
//
122+
// 1234: /some/path
123+
// Address Kbytes Mode Offset Device Mapping
124+
// 000073eb5f4c7000 8 rw--- 0000000000036000 008:00008 ld-linux-x86-64.so.2
125+
// 00007ffd588fc000 132 rw--- 0000000000000000 000:00000 [ stack ]
126+
// ffffffffff600000 4 --x-- 0000000000000000 000:00000 [ anon ]
127+
// ...
128+
// mapped: 3060K writeable/private: 348K shared: 0K
129+
#[cfg(target_os = "linux")]
130+
fn assert_device_format(pid: u32, s: &str) {
131+
let lines: Vec<_> = s.lines().collect();
132+
let line_count = lines.len();
133+
134+
let re = Regex::new(&format!("^{pid}: .+[^ ]$")).unwrap();
135+
assert!(re.is_match(lines[0]));
136+
137+
let expected_header = "Address Kbytes Mode Offset Device Mapping";
138+
assert_eq!(expected_header, lines[1]);
139+
140+
let re = Regex::new(
141+
r"^[0-9a-f]{16} +[1-9][0-9]* (-|r)(-|w)(-|x)(-|s)- [0-9a-f]{16} [0-9a-f]{3}:[0-9a-f]{5} ( \[ (anon|stack) \]|[a-zA-Z0-9._-]+)$",
142+
)
143+
.unwrap();
144+
145+
for line in lines.iter().take(line_count - 1).skip(2) {
146+
assert!(re.is_match(line), "failing line: {line}");
147+
}
148+
149+
let re = Regex::new(r"^mapped: \d+K\s{4}writeable/private: \d+K\s{4}shared: \d+K$").unwrap();
150+
assert!(
151+
re.is_match(lines[line_count - 1]),
152+
"failing line: {}",
153+
lines[line_count - 1]
154+
);
155+
}

0 commit comments

Comments
 (0)