1use std::time::Duration;
9
10use crate::client::Client;
11use crate::error::{Error, Result};
12use crate::sftp::Sftp;
13
14pub const FRESH_MODE: u32 = 0o711;
16
17pub fn parse_mode(m: &str) -> std::result::Result<u32, String> {
19 u32::from_str_radix(m.trim().trim_start_matches("0o"), 8)
20 .ok()
21 .filter(|v| *v <= 0o7777)
22 .ok_or_else(|| format!("mode {m:?} is not an octal mode like 0770"))
23}
24
25pub fn numeric_ids(owner: &str) -> Option<(u32, u32)> {
27 let (u, g) = owner.split_once(':').unwrap_or((owner, owner));
28 Some((u.parse().ok()?, g.parse().ok()?))
29}
30
31#[derive(Debug, Clone, Default, PartialEq)]
33pub struct Fix<'a> {
34 pub path: &'a str,
35 pub owner: Option<&'a str>,
37 pub mode: Option<u32>,
38 pub fresh_only: bool,
41}
42
43pub fn apply(client: &Client, name: &str, fix: &Fix) -> Result<bool> {
46 let step = |what: &str| format!("{what} {} in {name}", fix.path);
47 let resolved = match fix.owner {
48 Some(o) => Some(
49 resolve(client, name, o).map_err(|e| Error::OperationFailed {
50 step: step(&format!("chown {o}")),
51 message: e,
52 })?,
53 ),
54 None => None,
55 };
56 let mut sftp = Sftp::open(client, name, Duration::from_secs(30))?;
57 let st = sftp.stat(fix.path)?.ok_or_else(|| Error::OperationFailed {
58 step: step("stat"),
59 message: "the mount point does not exist".into(),
60 })?;
61 if fix.fresh_only && (st.uid != 0 || (st.mode != FRESH_MODE && Some(st.mode) != fix.mode)) {
62 return Ok(false);
63 }
64 if let Some(r) = &resolved {
65 sftp.chown(fix.path, r.uid, r.gid)?;
66 if let Some(home) = r.home.as_deref().filter(|h| !h.is_empty() && *h != "/") {
69 if fix
70 .path
71 .starts_with(&format!("{}/", home.trim_end_matches('/')))
72 {
73 let mut d = parent(fix.path);
74 while d != home.trim_end_matches('/') && d != "/" {
75 match sftp.stat(&d)? {
76 Some(s) if s.uid == 0 => sftp.chown(&d, r.uid, r.gid)?,
77 _ => break,
78 }
79 d = parent(&d);
80 }
81 }
82 }
83 }
84 if let Some(m) = fix.mode {
85 sftp.chmod(fix.path, m)?;
86 }
87 Ok(true)
88}
89
90fn parent(p: &str) -> String {
91 match p.trim_end_matches('/').rsplit_once('/') {
92 Some(("", _)) | None => "/".into(),
93 Some((d, _)) => d.into(),
94 }
95}
96
97#[derive(Debug, PartialEq)]
98struct Ids {
99 uid: u32,
100 gid: u32,
101 home: Option<String>,
102}
103
104fn resolve(client: &Client, name: &str, owner: &str) -> std::result::Result<Ids, String> {
105 let read = |path: &str| -> std::result::Result<String, String> {
106 match client.read_file(name, path) {
107 Ok(b) => Ok(String::from_utf8_lossy(&b.unwrap_or_default()).into_owned()),
108 Err(e) => Err(format!("read {path}: {e}")),
109 }
110 };
111 let (_, group) = split(owner);
112 let passwd = read("/etc/passwd")?;
114 let groups = match group {
115 Some(g) if g.parse::<u32>().is_err() => read("/etc/group")?,
116 _ => String::new(),
117 };
118 resolve_in(owner, &passwd, &groups)
119}
120
121fn split(owner: &str) -> (&str, Option<&str>) {
122 match owner.split_once(':') {
123 Some((u, g)) => (u, Some(g)),
124 None => (owner, None),
125 }
126}
127
128fn resolve_in(owner: &str, passwd: &str, group: &str) -> std::result::Result<Ids, String> {
131 let (user, grp) = split(owner);
132 let entry = passwd.lines().find_map(|l| {
133 let f: Vec<&str> = l.split(':').collect();
134 (f.len() >= 6 && (f[0] == user || (user.parse::<u32>().is_ok() && f[2] == user)))
135 .then_some(f)
136 });
137 let mut ids = match entry {
138 Some(f) => Ids {
139 uid: f[2]
140 .parse()
141 .map_err(|_| format!("bad uid for {user} in /etc/passwd"))?,
142 gid: f[3]
143 .parse()
144 .map_err(|_| format!("bad gid for {user} in /etc/passwd"))?,
145 home: Some(f[5].to_string()),
146 },
147 None => match user.parse() {
148 Ok(n) => Ids {
149 uid: n,
150 gid: n,
151 home: None,
152 },
153 Err(_) => return Err(format!("no such user in the image's /etc/passwd: {user}")),
154 },
155 };
156 match grp {
157 None | Some("") => {}
158 Some(g) => {
159 ids.gid = match g.parse() {
160 Ok(n) => n,
161 Err(_) => group
162 .lines()
163 .map(|l| l.split(':').collect::<Vec<_>>())
164 .find(|f| f.len() >= 3 && f[0] == g)
165 .and_then(|f| f[2].parse().ok())
166 .ok_or_else(|| format!("no such group in the image's /etc/group: {g}"))?,
167 }
168 }
169 }
170 Ok(ids)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 const PASSWD: &str = "root:x:0:0:root:/root:/bin/sh\ndev:x:1000:1000::/home/dev:/bin/bash\napp:x:999:998::/:/sbin/nologin\n";
178 const GROUP: &str = "root:x:0:\nstaff:x:50:dev\n";
179
180 fn ids(uid: u32, gid: u32, home: Option<&str>) -> Ids {
181 Ids {
182 uid,
183 gid,
184 home: home.map(String::from),
185 }
186 }
187
188 #[test]
189 fn owners_resolve_as_getent_and_chown_do() {
190 assert_eq!(
191 resolve_in("dev", PASSWD, GROUP),
192 Ok(ids(1000, 1000, Some("/home/dev")))
193 );
194 assert_eq!(
195 resolve_in("1000", PASSWD, GROUP),
196 Ok(ids(1000, 1000, Some("/home/dev")))
197 );
198 assert_eq!(
199 resolve_in("dev:staff", PASSWD, GROUP),
200 Ok(ids(1000, 50, Some("/home/dev")))
201 );
202 assert_eq!(resolve_in("app:7", PASSWD, ""), Ok(ids(999, 7, Some("/"))));
203 assert_eq!(resolve_in("4242", PASSWD, ""), Ok(ids(4242, 4242, None)));
205 assert_eq!(resolve_in("4242:7", "", ""), Ok(ids(4242, 7, None)));
206 let e = resolve_in("nobody", PASSWD, GROUP).unwrap_err();
207 assert!(e.contains("no such user") && e.contains("nobody"), "{e}");
208 let e = resolve_in("dev:wheel", PASSWD, GROUP).unwrap_err();
209 assert!(e.contains("no such group") && e.contains("wheel"), "{e}");
210 }
211
212 #[test]
213 fn modes_and_numeric_ids() {
214 assert_eq!(parse_mode("0770"), Ok(0o770));
215 assert_eq!(parse_mode("2775"), Ok(0o2775));
216 assert_eq!(parse_mode("0o750"), Ok(0o750));
217 assert!(parse_mode("0890").is_err());
218 assert!(parse_mode("17777").is_err());
219 assert_eq!(numeric_ids("1000"), Some((1000, 1000)));
220 assert_eq!(numeric_ids("1000:50"), Some((1000, 50)));
221 assert_eq!(numeric_ids("dev"), None);
222 assert_eq!(numeric_ids("1000:staff"), None);
223 }
224
225 #[test]
226 fn parents() {
227 assert_eq!(parent("/home/dev/.cache/x"), "/home/dev/.cache");
228 assert_eq!(parent("/data"), "/");
229 assert_eq!(parent("/data/"), "/");
230 }
231}