gioser

tawasuyu/arje

Rust 52TOML 25Markdown 2Shell 2712 Definiciones

⎇ main crates/ente-tmpfiles-compat/src/main.rs Rust · 281 líneas · 9.3 KB Historia Quién tocó qué crudo

ente-policy-provider, ente-tmpfiles-compat, journald export format 883c14dad · Sergio ·

1//! ente-tmpfiles-compat: aplica directivas tmpfiles.d al boot.
2//!
3//! Lee, en orden, los conf files de:
4//!   /usr/lib/tmpfiles.d/*.conf
5//!   /etc/tmpfiles.d/*.conf       (override del usuario, gana)
6//!   /run/tmpfiles.d/*.conf       (efímero)
7//!
8//! Aplica un subset de directivas — las suficientes para el boot:
9//!   d  — crear directorio (idempotente: no falla si existe)
10//!   D  — crear directorio + limpiar contenido si existe
11//!   f  — crear archivo (vacío, perms aplicados)
12//!   L  — crear symlink (overrideable con `+L` si existe)
13//!   r  — remove file (no falla si ausente)
14//!   R  — remove recursivamente
15//!   e  — adjust perms si existe
16//!
17//! Edad/cleanup (`age` field) y modos exotic (b, c, p, P) se ignoran.
18//! El proceso es OneShot: corre, aplica, sale con código 0 / 1.
19
20use std::collections::BTreeMap;
21use std::fs;
22use std::os::unix::fs::PermissionsExt;
23use std::path::{Path, PathBuf};
24use tracing::{debug, info, warn};
25use tracing_subscriber::EnvFilter;
26
27const SEARCH_DIRS: &[&str] = &[
28    "/usr/lib/tmpfiles.d",
29    "/etc/tmpfiles.d",
30    "/run/tmpfiles.d",
31];
32
33#[derive(Debug, Clone)]
34struct Directive {
35    typ: char,           // d, D, f, L, r, R, e
36    path: PathBuf,
37    mode: Option<u32>,
38    user: Option<String>,
39    group: Option<String>,
40    arg: Option<String>, // symlink target o content
41}
42
43fn main() {
44    init_tracing();
45    info!("ente-tmpfiles-compat: aplicando directivas tmpfiles.d");
46    let directives = collect_directives();
47    info!(count = directives.len(), "directivas a aplicar");
48
49    let mut applied = 0;
50    let mut skipped = 0;
51    let mut errors = 0;
52    for d in directives {
53        match apply(&d) {
54            Ok(true) => applied += 1,
55            Ok(false) => skipped += 1,
56            Err(e) => {
57                warn!(?e, ?d.typ, path = %d.path.display(), "directiva falló");
58                errors += 1;
59            }
60        }
61    }
62    info!(applied, skipped, errors, "tmpfiles aplicado");
63    if errors > 0 { std::process::exit(1); }
64}
65
66fn collect_directives() -> Vec<Directive> {
67    // Last-wins por path: /etc supera /usr/lib, /run supera /etc.
68    let mut by_path: BTreeMap<(PathBuf, char), Directive> = BTreeMap::new();
69    for dir in SEARCH_DIRS {
70        if !Path::new(dir).exists() { continue; }
71        let mut entries: Vec<_> = match fs::read_dir(dir) {
72            Ok(rd) => rd.filter_map(|e| e.ok()).collect(),
73            Err(_) => continue,
74        };
75        entries.sort_by_key(|e| e.file_name());
76        for entry in entries {
77            let path = entry.path();
78            if path.extension().map(|e| e != "conf").unwrap_or(true) { continue; }
79            match fs::read_to_string(&path) {
80                Ok(content) => {
81                    for (line_no, line) in content.lines().enumerate() {
82                        if let Some(d) = parse_line(line) {
83                            by_path.insert((d.path.clone(), d.typ), d);
84                        } else if !line.trim().is_empty() && !line.trim().starts_with('#') {
85                            debug!(file = %path.display(), line_no, line, "no parseable, skip");
86                        }
87                    }
88                }
89                Err(e) => warn!(?e, path = %path.display(), "read"),
90            }
91        }
92    }
93    // Orden de aplicación: removes (r/R) primero, luego creates (d/D/f/L),
94    // adjusts (e) al final.
95    let mut all: Vec<Directive> = by_path.into_values().collect();
96    all.sort_by_key(|d| match d.typ {
97        'r' | 'R' => 0,
98        'd' | 'D' | 'f' | 'L' => 1,
99        'e' => 2,
100        _ => 3,
101    });
102    all
103}
104
105fn parse_line(line: &str) -> Option<Directive> {
106    let line = line.trim();
107    if line.is_empty() || line.starts_with('#') { return None; }
108    // Formato: TYPE PATH MODE USER GROUP AGE ARGUMENT
109    // Strip leading '+' (override marker) y '!' (boot-only) — los soportamos
110    // implícitamente.
111    let typ_str = line.chars().next()?;
112    let typ = match typ_str {
113        '+' | '!' => line.chars().nth(1)?,
114        c => c,
115    };
116    if !"dDfLrRe".contains(typ) { return None; }
117    // tokenize tomando en cuenta '-' como "default"
118    let mut parts = line.splitn(7, char::is_whitespace).filter(|s| !s.is_empty());
119    let _t = parts.next()?;
120    let path = parts.next()?.to_string();
121    let mode = parts.next().and_then(parse_mode);
122    let user = parts.next().and_then(parse_default);
123    let group = parts.next().and_then(parse_default);
124    let _age = parts.next();
125    let arg = parts.next().and_then(parse_default);
126    Some(Directive {
127        typ,
128        path: PathBuf::from(path),
129        mode, user, group, arg,
130    })
131}
132
133fn parse_default(s: &str) -> Option<String> {
134    if s == "-" { None } else { Some(s.to_string()) }
135}
136
137fn parse_mode(s: &str) -> Option<u32> {
138    if s == "-" { return None; }
139    u32::from_str_radix(s.trim_start_matches('~'), 8).ok()
140}
141
142fn apply(d: &Directive) -> anyhow::Result<bool> {
143    match d.typ {
144        'd' | 'D' => apply_d(d),
145        'f' => apply_f(d),
146        'L' => apply_l(d),
147        'r' => apply_r(d, false),
148        'R' => apply_r(d, true),
149        'e' => apply_e(d),
150        _ => Ok(false),
151    }
152}
153
154fn apply_d(d: &Directive) -> anyhow::Result<bool> {
155    fs::create_dir_all(&d.path)
156        .map_err(|e| anyhow::anyhow!("mkdir {}: {e}", d.path.display()))?;
157    if let Some(mode) = d.mode {
158        fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?;
159    }
160    chown(&d.path, d.user.as_deref(), d.group.as_deref())?;
161    if d.typ == 'D' {
162        // Limpiar contenido (no recursivo).
163        if let Ok(rd) = fs::read_dir(&d.path) {
164            for entry in rd.flatten() {
165                let p = entry.path();
166                if p.is_dir() { let _ = fs::remove_dir_all(&p); }
167                else { let _ = fs::remove_file(&p); }
168            }
169        }
170    }
171    info!(path = %d.path.display(), mode = ?d.mode, "d/D aplicado");
172    Ok(true)
173}
174
175fn apply_f(d: &Directive) -> anyhow::Result<bool> {
176    if !d.path.exists() {
177        if let Some(parent) = d.path.parent() {
178            let _ = fs::create_dir_all(parent);
179        }
180        let content = d.arg.clone().unwrap_or_default();
181        fs::write(&d.path, content.as_bytes())?;
182    }
183    if let Some(mode) = d.mode {
184        fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?;
185    }
186    chown(&d.path, d.user.as_deref(), d.group.as_deref())?;
187    info!(path = %d.path.display(), mode = ?d.mode, "f aplicado");
188    Ok(true)
189}
190
191fn apply_l(d: &Directive) -> anyhow::Result<bool> {
192    let target = match &d.arg {
193        Some(t) => t,
194        None => anyhow::bail!("L sin target en {}", d.path.display()),
195    };
196    if d.path.exists() {
197        // No sobreescribimos symlinks/files existentes (modo no-`+`).
198        debug!(path = %d.path.display(), "L: existe, skip");
199        return Ok(false);
200    }
201    if let Some(parent) = d.path.parent() {
202        let _ = fs::create_dir_all(parent);
203    }
204    std::os::unix::fs::symlink(target, &d.path)?;
205    info!(path = %d.path.display(), %target, "L aplicado");
206    Ok(true)
207}
208
209fn apply_r(d: &Directive, recursive: bool) -> anyhow::Result<bool> {
210    if !d.path.exists() {
211        return Ok(false);
212    }
213    if recursive {
214        fs::remove_dir_all(&d.path)?;
215    } else if d.path.is_dir() {
216        fs::remove_dir(&d.path)?;
217    } else {
218        fs::remove_file(&d.path)?;
219    }
220    info!(path = %d.path.display(), recursive, "remove aplicado");
221    Ok(true)
222}
223
224fn apply_e(d: &Directive) -> anyhow::Result<bool> {
225    if !d.path.exists() {
226        return Ok(false);
227    }
228    if let Some(mode) = d.mode {
229        fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?;
230    }
231    chown(&d.path, d.user.as_deref(), d.group.as_deref())?;
232    info!(path = %d.path.display(), "e aplicado");
233    Ok(true)
234}
235
236fn chown(path: &Path, user: Option<&str>, group: Option<&str>) -> anyhow::Result<()> {
237    use std::ffi::CString;
238    let uid = match user {
239        Some(u) => Some(lookup_uid(u)?),
240        None => None,
241    };
242    let gid = match group {
243        Some(g) => Some(lookup_gid(g)?),
244        None => None,
245    };
246    let (uid, gid) = (uid.unwrap_or(u32::MAX), gid.unwrap_or(u32::MAX));
247    let cstr = CString::new(path.as_os_str().as_encoded_bytes())?;
248    let r = unsafe { libc::chown(cstr.as_ptr(), uid, gid) };
249    if r != 0 {
250        let e = std::io::Error::last_os_error();
251        // No-op si ya somos non-root y el chown falla con EPERM.
252        if e.raw_os_error() == Some(libc::EPERM) {
253            debug!(path = %path.display(), "chown EPERM (esperado sin root)");
254            return Ok(());
255        }
256        return Err(anyhow::anyhow!("chown: {e}"));
257    }
258    Ok(())
259}
260
261fn lookup_uid(name: &str) -> anyhow::Result<u32> {
262    if let Ok(n) = name.parse::<u32>() { return Ok(n); }
263    let cstr = std::ffi::CString::new(name)?;
264    let pw = unsafe { libc::getpwnam(cstr.as_ptr()) };
265    if pw.is_null() { anyhow::bail!("user '{name}' no encontrado"); }
266    Ok(unsafe { (*pw).pw_uid })
267}
268
269fn lookup_gid(name: &str) -> anyhow::Result<u32> {
270    if let Ok(n) = name.parse::<u32>() { return Ok(n); }
271    let cstr = std::ffi::CString::new(name)?;
272    let gr = unsafe { libc::getgrnam(cstr.as_ptr()) };
273    if gr.is_null() { anyhow::bail!("group '{name}' no encontrado"); }
274    Ok(unsafe { (*gr).gr_gid })
275}
276
277fn init_tracing() {
278    let filter = EnvFilter::try_from_default_env()
279        .unwrap_or_else(|_| EnvFilter::new("ente_tmpfiles_compat=info"));
280    tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
281}